├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── api.env.example ├── app.js ├── config ├── db.js └── jwt.js ├── controllers ├── transaction.controller.js ├── user.controller.js └── wallet.controller.js ├── docker-compose.yml ├── helpers ├── json │ └── banks.json └── payment.helpers.js ├── knexfile.js ├── middlewares ├── auth.js ├── error-handler.js └── set-wallet-pin.js ├── migrations ├── 20220226055100_users.js ├── 20220226091212_wallets.js └── 20220227082352_transactions.js ├── package-lock.json ├── package.json ├── release-tasks.sh ├── routes ├── index.js ├── transaction.route.js ├── user.route.js └── wallet.route.js ├── server.js ├── services ├── index.js ├── transaction.service.js ├── user.service.js └── wallet.service.js ├── start-script.sh ├── tests ├── transaction.test.js ├── user.test.js └── wallet.test.js ├── utils ├── catchasync.js └── errors │ ├── app.error.js │ ├── badrequest.error.js │ ├── notfound.error.js │ └── unauthorized.error.js └── validations ├── index.js ├── user.validation.js └── wallet.validation.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | HOST=localhost 3 | APP_URL=http://localhost:3000 4 | NODE_ENV=development 5 | APP_SECRET_KEY=secret 6 | FLUTTERWAVE_KEY=YOUR FLUTTERWAVE SECRECT KEY 7 | 8 | # DATABASE DETAILS 9 | DB_NAME=YOUR DATABASE NAME 10 | DB_USER=YOUR DATABASE USER 11 | DB_PASSWORD=YOUR DATABASE PASSWORD 12 | DB_PORT=YOUR DATABASE PORT 13 | 14 | # TEST DATABASE DETAILS 15 | TEST_DB_NAME=YOUR DATABASE NAME 16 | TEST_DB_USER=YOUR DATABASE USER 17 | TEST_DB_PASSWORD=YOUR DATABASE PASSWORD 18 | TEST_DB_PORT=YOUR DATABASE PORT -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | .env 4 | coverage/ 5 | password.txt 6 | api.env 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 3000 12 | 13 | RUN chmod +x ./start-script.sh 14 | 15 | ENTRYPOINT [ "./start-script.sh" ] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Salami Usman Olawale 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: ./release-tasks.sh 2 | web: ENV_SILENT=true npm start -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # e-wallet-system 2 | This system allow users to fund their account, transfer funds and withdraw from their account. 3 | 4 | # Features 5 | - Basic Authentication (Register & Login) 6 | - Get Profile 7 | - Set Wallet Pin 8 | - Fund Wallet 9 | - Verify Wallet Funding 10 | - Fund Transfer 11 | - Withdraw Fund 12 | - Get Transactions 13 | 14 | # API Documentation 15 | https://documenter.getpostman.com/view/5916628/UVkqrEs8 16 | 17 | # How to install 18 | 19 | ## Using Git (recommended) 20 | 1. Clone the project from github. 21 | 22 | ``` 23 | git clone https://github.com/devwalex/e-wallet-system.git 24 | ``` 25 | 26 | ## Using manual download ZIP 27 | 28 | 1. Download repository 29 | 2. Uncompress to your desired directory 30 | 31 | ## Install npm dependencies 32 | 33 | ``` 34 | npm install 35 | ``` 36 | 37 | ## Setting up environments 38 | 1. You will find a file named `.env.example` on root directory of project. 39 | 2. Create a new file by copying and pasting the file and then renaming it to just `.env` 40 | 41 | ``` 42 | cp .env.example .env 43 | ``` 44 | 3. The file `.env` is already ignored, so you never commit your credentials. 45 | 4. Change the values of the file to your environment. Helpful comments added to `.env.example` file to understand the constants. 46 | 47 | ## Running and resetting migrations 48 | 49 | 1. To run migrations 50 | ``` 51 | npm run migrate 52 | ``` 53 | 2. To reset migrations 54 | ``` 55 | npm run migrate:reset 56 | ``` 57 | 58 | # How to run 59 | 60 | ## Running API server locally 61 | ``` 62 | npm start 63 | ``` 64 | You will know server is running by checking the output of the command `npm start` 65 | 66 | 67 | 68 | # Running Tests 69 | 70 | ``` 71 | npm test 72 | ``` 73 | **Note:** Make sure you set up the test variable in the `.env` file 74 | 75 | # Author 76 | Usman Salami 77 | 78 | # License 79 | MIT 80 | -------------------------------------------------------------------------------- /api.env.example: -------------------------------------------------------------------------------- 1 | APP_SECRET_KEY=secret 2 | FLUTTERWAVE_KEY=YOUR FLUTTERWAVE SECRECT KEY 3 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const helmet = require("helmet"); 3 | const xss = require("xss-clean"); 4 | const compression = require("compression"); 5 | const cors = require("cors"); 6 | const routes = require("./routes"); 7 | const errorHandler = require("./middlewares/error-handler"); 8 | const NotFoundError = require("./utils/errors/notfound.error"); 9 | require("dotenv/config"); 10 | 11 | const app = express(); 12 | 13 | // set security HTTP headers 14 | app.use(helmet()); 15 | 16 | // parse json request body 17 | app.use(express.json()); 18 | 19 | // parse urlencoded request body 20 | app.use(express.urlencoded({ extended: true })); 21 | 22 | // sanitize request data 23 | app.use(xss()); 24 | 25 | // gzip compression 26 | app.use(compression()); 27 | 28 | // enable cors 29 | app.use(cors()); 30 | app.options("*", cors()); 31 | 32 | app.use(routes); 33 | 34 | // return a NotFoundError for any unknown api request 35 | app.use((req, res, next) => { 36 | next(new NotFoundError(`Cannot ${req.method} ${req.originalUrl}`)); 37 | }); 38 | 39 | app.use(errorHandler); 40 | 41 | module.exports = app; 42 | -------------------------------------------------------------------------------- /config/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv/config'); 2 | 3 | const environment = process.env.NODE_ENV || 'development'; 4 | const configuration = require('../knexfile')[environment]; 5 | const db = require('knex')(configuration); 6 | const { attachPaginate } = require('knex-paginate'); 7 | attachPaginate(); 8 | module.exports = db -------------------------------------------------------------------------------- /config/jwt.js: -------------------------------------------------------------------------------- 1 | require('dotenv/config'); 2 | 3 | const jwtConfig = { 4 | appKey: process.env.APP_SECRET_KEY || 'secret', 5 | }; 6 | 7 | module.exports = jwtConfig 8 | -------------------------------------------------------------------------------- /controllers/transaction.controller.js: -------------------------------------------------------------------------------- 1 | const { transactionService } = require("../services"); 2 | const httpStatus = require("http-status"); 3 | const catchAsync = require("../utils/catchasync"); 4 | 5 | const getTransactions = catchAsync(async (req, res) => { 6 | const transactionData = { 7 | userId: req.user.id, 8 | limit: req.query.limit, 9 | page: req.query.page, 10 | }; 11 | const transactions = await transactionService.getTransactions(transactionData); 12 | 13 | return res.status(httpStatus.OK).send({ 14 | success: true, 15 | message: "Returned transactions successfully", 16 | result: transactions, 17 | }); 18 | }); 19 | 20 | module.exports = { 21 | getTransactions, 22 | }; 23 | -------------------------------------------------------------------------------- /controllers/user.controller.js: -------------------------------------------------------------------------------- 1 | const { userService, walletService } = require("../services"); 2 | const httpStatus = require("http-status"); 3 | const { validationResult } = require("express-validator"); 4 | const bcrypt = require("bcryptjs"); 5 | const jwt = require("jsonwebtoken"); 6 | const jwtConfig = require("../config/jwt"); 7 | const catchAsync = require("../utils/catchasync"); 8 | const UnAuthorizedError = require("../utils/errors/unauthorized.error"); 9 | 10 | const register = catchAsync(async (req, res) => { 11 | const errors = validationResult(req); 12 | if (!errors.isEmpty()) { 13 | return res.status(httpStatus.BAD_REQUEST).json({ success: false, errors: errors.array() }); 14 | } 15 | const user = await userService.createUser(req.body); 16 | 17 | await walletService.createWallet(user[0]); 18 | 19 | return res.status(httpStatus.CREATED).send({ 20 | success: true, 21 | message: "Registered successfully!", 22 | }); 23 | }); 24 | 25 | const login = catchAsync(async (req, res) => { 26 | const errors = validationResult(req); 27 | if (!errors.isEmpty()) { 28 | return res.status(httpStatus.BAD_REQUEST).json({ success: false, errors: errors.array() }); 29 | } 30 | const { email, password } = req.body; 31 | const user = await userService.findUserByEmail(email); 32 | 33 | if (!user) { 34 | throw new UnAuthorizedError("Invalid email or password"); 35 | } 36 | 37 | const match = await bcrypt.compare(password, user.password); 38 | 39 | if (!match) { 40 | throw new UnAuthorizedError("Invalid email or password"); 41 | } 42 | 43 | const payload = { 44 | id: user.id, 45 | first_name: user.first_name, 46 | last_name: user.last_name, 47 | email: user.email, 48 | }; 49 | 50 | const token = jwt.sign(payload, jwtConfig.appKey, { 51 | expiresIn: "1h", 52 | }); 53 | 54 | return res.status(httpStatus.OK).send({ 55 | success: true, 56 | message: "Logged in successfully!", 57 | results: payload, 58 | token, 59 | }); 60 | }); 61 | 62 | const getProfile = catchAsync(async (req, res) => { 63 | const user = await userService.getProfile(req.user); 64 | 65 | return res.status(httpStatus.OK).send({ 66 | success: true, 67 | message: "Returned profile successfully", 68 | result: user, 69 | }); 70 | }); 71 | 72 | module.exports = { 73 | register, 74 | login, 75 | getProfile, 76 | }; 77 | -------------------------------------------------------------------------------- /controllers/wallet.controller.js: -------------------------------------------------------------------------------- 1 | const { walletService } = require("../services"); 2 | const httpStatus = require("http-status"); 3 | const { validationResult } = require("express-validator"); 4 | const catchAsync = require("../utils/catchasync"); 5 | const BadRequestError = require("../utils/errors/badrequest.error"); 6 | 7 | const setWalletPin = catchAsync(async (req, res) => { 8 | const errors = validationResult(req); 9 | if (!errors.isEmpty()) { 10 | return res.status(httpStatus.BAD_REQUEST).json({ success: false, errors: errors.array() }); 11 | } 12 | 13 | const { pin } = req.body; 14 | 15 | const walletData = { 16 | pin, 17 | user: req.user, 18 | }; 19 | 20 | await walletService.setWalletPin(walletData); 21 | 22 | return res.status(httpStatus.CREATED).send({ 23 | success: true, 24 | message: "Set pin successfully!", 25 | }); 26 | }); 27 | 28 | const fundWallet = catchAsync(async (req, res) => { 29 | const errors = validationResult(req); 30 | if (!errors.isEmpty()) { 31 | return res.status(httpStatus.BAD_REQUEST).json({ success: false, errors: errors.array() }); 32 | } 33 | 34 | const { amount, frontend_base_url } = req.body; 35 | 36 | const walletData = { 37 | amount, 38 | user: req.user, 39 | frontend_base_url, 40 | }; 41 | 42 | const paymentLink = await walletService.fundWallet(walletData); 43 | 44 | return res.status(httpStatus.CREATED).send({ 45 | success: true, 46 | message: "Initialized Wallet Funding", 47 | paymentLink, 48 | }); 49 | }); 50 | 51 | const verifyWalletFunding = catchAsync(async (req, res) => { 52 | const { transaction_id, status, tx_ref } = req.query; 53 | 54 | if (!transaction_id || !status || !tx_ref) { 55 | throw new BadRequestError("Could not verify payment"); 56 | } 57 | 58 | const walletData = { 59 | transaction_id, 60 | status, 61 | tx_ref, 62 | user: req.user, 63 | }; 64 | 65 | await walletService.verifyWalletFunding(walletData); 66 | 67 | return res.status(httpStatus.CREATED).send({ 68 | success: true, 69 | message: "Wallet Funded Successfully", 70 | }); 71 | }); 72 | 73 | const transferFund = catchAsync(async (req, res) => { 74 | const errors = validationResult(req); 75 | if (!errors.isEmpty()) { 76 | return res.status(httpStatus.BAD_REQUEST).json({ success: false, errors: errors.array() }); 77 | } 78 | const { amount, wallet_code_or_email, wallet_pin } = req.body; 79 | 80 | const walletData = { 81 | amount, 82 | wallet_code_or_email, 83 | wallet_pin, 84 | user: req.user, 85 | }; 86 | 87 | await walletService.transferFund(walletData); 88 | 89 | return res.status(httpStatus.CREATED).send({ 90 | success: true, 91 | message: "Fund Transfer Successful", 92 | }); 93 | }); 94 | 95 | const withdrawFund = catchAsync(async (req, res) => { 96 | const errors = validationResult(req); 97 | if (!errors.isEmpty()) { 98 | return res.status(httpStatus.BAD_REQUEST).json({ success: false, errors: errors.array() }); 99 | } 100 | const { amount, bank_code, account_number, wallet_pin } = req.body; 101 | 102 | const walletData = { 103 | amount, 104 | bank_code, 105 | account_number, 106 | wallet_pin, 107 | user: req.user, 108 | }; 109 | 110 | await walletService.withdrawFund(walletData); 111 | 112 | return res.status(httpStatus.CREATED).send({ 113 | success: true, 114 | message: "Withdrawal Successful", 115 | }); 116 | }); 117 | 118 | const getWalletBalance = catchAsync(async (req, res) => { 119 | const walletData = { 120 | user: req.user, 121 | }; 122 | 123 | const wallet = await walletService.getWalletBalance(walletData); 124 | 125 | return res.status(httpStatus.OK).send({ 126 | success: true, 127 | message: "Returned wallet balance successfully", 128 | result: wallet.balance, 129 | }); 130 | }); 131 | 132 | const getBanks = catchAsync(async (req, res) => { 133 | const banks = walletService.getBanks(); 134 | 135 | return res.status(httpStatus.OK).send({ 136 | success: true, 137 | message: "Returned banks successfully", 138 | result: banks, 139 | }); 140 | }); 141 | 142 | module.exports = { 143 | setWalletPin, 144 | fundWallet, 145 | verifyWalletFunding, 146 | transferFund, 147 | withdrawFund, 148 | getWalletBalance, 149 | getBanks, 150 | }; 151 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | db: 4 | image: mysql:8.0 5 | restart: always 6 | secrets: 7 | - db-password 8 | volumes: 9 | - db-data:/var/lib/mysql 10 | environment: 11 | - MYSQL_DATABASE=e_wallet_system 12 | - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password 13 | networks: 14 | - app-network 15 | 16 | api: 17 | build: . 18 | restart: always 19 | ports: 20 | - "3000:3000" 21 | depends_on: 22 | - db 23 | environment: 24 | - PORT=3000 25 | - HOST=localhost 26 | - APP_URL=http://localhost:3000 27 | - NODE_ENV=development 28 | - DB_NAME=e_wallet_system 29 | - DB_USER=root 30 | - DB_PASSWORD_FILE=/run/secrets/db-password 31 | - DB_HOST=db 32 | - DB_PORT=3306 33 | env_file: 34 | - api.env 35 | secrets: 36 | - db-password 37 | networks: 38 | - app-network 39 | 40 | volumes: 41 | db-data: 42 | secrets: 43 | db-password: 44 | file: password.txt 45 | networks: 46 | app-network: 47 | driver: bridge 48 | -------------------------------------------------------------------------------- /helpers/json/banks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Abbey Mortgage Bank", 4 | "slug": "abbey-mortgage-bank", 5 | "code": "801", 6 | "longcode": "", 7 | "gateway": null, 8 | "pay_with_bank": false, 9 | "active": true, 10 | "is_deleted": false, 11 | "country": "Nigeria", 12 | "currency": "NGN", 13 | "type": "nuban", 14 | "id": 174, 15 | "created_at": "2020-12-07T16:19:09.000Z", 16 | "updated_at": "2020-12-07T16:19:19.000Z" 17 | }, 18 | { 19 | "name": "Above Only MFB", 20 | "slug": "above-only-mfb", 21 | "code": "51204", 22 | "longcode": "", 23 | "gateway": null, 24 | "pay_with_bank": false, 25 | "active": true, 26 | "is_deleted": null, 27 | "country": "Nigeria", 28 | "currency": "NGN", 29 | "type": "nuban", 30 | "id": 188, 31 | "created_at": "2021-10-13T20:35:17.000Z", 32 | "updated_at": "2021-10-13T20:35:17.000Z" 33 | }, 34 | { 35 | "name": "Access Bank", 36 | "slug": "access-bank", 37 | "code": "044", 38 | "longcode": "044150149", 39 | "gateway": "emandate", 40 | "pay_with_bank": false, 41 | "active": true, 42 | "is_deleted": null, 43 | "country": "Nigeria", 44 | "currency": "NGN", 45 | "type": "nuban", 46 | "id": 1, 47 | "created_at": "2016-07-14T10:04:29.000Z", 48 | "updated_at": "2020-02-18T08:06:44.000Z" 49 | }, 50 | { 51 | "name": "Access Bank (Diamond)", 52 | "slug": "access-bank-diamond", 53 | "code": "063", 54 | "longcode": "063150162", 55 | "gateway": "emandate", 56 | "pay_with_bank": false, 57 | "active": true, 58 | "is_deleted": null, 59 | "country": "Nigeria", 60 | "currency": "NGN", 61 | "type": "nuban", 62 | "id": 3, 63 | "created_at": "2016-07-14T10:04:29.000Z", 64 | "updated_at": "2020-02-18T08:06:48.000Z" 65 | }, 66 | { 67 | "name": "ALAT by WEMA", 68 | "slug": "alat-by-wema", 69 | "code": "035A", 70 | "longcode": "035150103", 71 | "gateway": "emandate", 72 | "pay_with_bank": true, 73 | "active": true, 74 | "is_deleted": null, 75 | "country": "Nigeria", 76 | "currency": "NGN", 77 | "type": "nuban", 78 | "id": 27, 79 | "created_at": "2017-11-15T12:21:31.000Z", 80 | "updated_at": "2021-02-18T14:55:34.000Z" 81 | }, 82 | { 83 | "name": "Amju Unique MFB", 84 | "slug": "amju-unique-mfb", 85 | "code": "50926", 86 | "longcode": "511080896", 87 | "gateway": null, 88 | "pay_with_bank": false, 89 | "active": true, 90 | "is_deleted": null, 91 | "country": "Nigeria", 92 | "currency": "NGN", 93 | "type": "nuban", 94 | "id": 179, 95 | "created_at": "2021-07-07T13:45:57.000Z", 96 | "updated_at": "2021-07-07T13:45:57.000Z" 97 | }, 98 | { 99 | "name": "ASO Savings and Loans", 100 | "slug": "asosavings", 101 | "code": "401", 102 | "longcode": "", 103 | "gateway": null, 104 | "pay_with_bank": false, 105 | "active": true, 106 | "is_deleted": null, 107 | "country": "Nigeria", 108 | "currency": "NGN", 109 | "type": "nuban", 110 | "id": 63, 111 | "created_at": "2018-09-23T05:52:38.000Z", 112 | "updated_at": "2019-01-30T09:38:57.000Z" 113 | }, 114 | { 115 | "name": "Bainescredit MFB", 116 | "slug": "bainescredit-mfb", 117 | "code": "51229", 118 | "longcode": "", 119 | "gateway": null, 120 | "pay_with_bank": false, 121 | "active": true, 122 | "is_deleted": null, 123 | "country": "Nigeria", 124 | "currency": "NGN", 125 | "type": "nuban", 126 | "id": 181, 127 | "created_at": "2021-07-12T14:41:18.000Z", 128 | "updated_at": "2021-07-12T14:41:18.000Z" 129 | }, 130 | { 131 | "name": "Bowen Microfinance Bank", 132 | "slug": "bowen-microfinance-bank", 133 | "code": "50931", 134 | "longcode": "", 135 | "gateway": null, 136 | "pay_with_bank": false, 137 | "active": true, 138 | "is_deleted": false, 139 | "country": "Nigeria", 140 | "currency": "NGN", 141 | "type": "nuban", 142 | "id": 108, 143 | "created_at": "2020-02-11T15:38:57.000Z", 144 | "updated_at": "2020-02-11T15:38:57.000Z" 145 | }, 146 | { 147 | "name": "Carbon", 148 | "slug": "carbon", 149 | "code": "565", 150 | "longcode": "", 151 | "gateway": null, 152 | "pay_with_bank": false, 153 | "active": true, 154 | "is_deleted": false, 155 | "country": "Nigeria", 156 | "currency": "NGN", 157 | "type": "nuban", 158 | "id": 82, 159 | "created_at": "2020-06-16T08:15:31.000Z", 160 | "updated_at": "2021-08-05T15:25:01.000Z" 161 | }, 162 | { 163 | "name": "CEMCS Microfinance Bank", 164 | "slug": "cemcs-microfinance-bank", 165 | "code": "50823", 166 | "longcode": "", 167 | "gateway": null, 168 | "pay_with_bank": false, 169 | "active": true, 170 | "is_deleted": false, 171 | "country": "Nigeria", 172 | "currency": "NGN", 173 | "type": "nuban", 174 | "id": 74, 175 | "created_at": "2020-03-23T15:06:13.000Z", 176 | "updated_at": "2020-03-23T15:06:28.000Z" 177 | }, 178 | { 179 | "name": "Chanelle Microfinance Bank Limited", 180 | "slug": "chanelle-microfinance-bank-limited-ng", 181 | "code": "50171", 182 | "longcode": "50171", 183 | "gateway": "", 184 | "pay_with_bank": false, 185 | "active": true, 186 | "is_deleted": false, 187 | "country": "Nigeria", 188 | "currency": "NGN", 189 | "type": "nuban", 190 | "id": 284, 191 | "created_at": "2022-02-10T13:28:38.000Z", 192 | "updated_at": "2022-02-10T13:28:38.000Z" 193 | }, 194 | { 195 | "name": "Citibank Nigeria", 196 | "slug": "citibank-nigeria", 197 | "code": "023", 198 | "longcode": "023150005", 199 | "gateway": null, 200 | "pay_with_bank": false, 201 | "active": true, 202 | "is_deleted": null, 203 | "country": "Nigeria", 204 | "currency": "NGN", 205 | "type": "nuban", 206 | "id": 2, 207 | "created_at": "2016-07-14T10:04:29.000Z", 208 | "updated_at": "2020-02-18T20:24:02.000Z" 209 | }, 210 | { 211 | "name": "Corestep MFB", 212 | "slug": "corestep-mfb", 213 | "code": "50204", 214 | "longcode": "", 215 | "gateway": null, 216 | "pay_with_bank": false, 217 | "active": true, 218 | "is_deleted": null, 219 | "country": "Nigeria", 220 | "currency": "NGN", 221 | "type": "nuban", 222 | "id": 283, 223 | "created_at": "2022-02-09T14:33:06.000Z", 224 | "updated_at": "2022-02-09T14:33:06.000Z" 225 | }, 226 | { 227 | "name": "Coronation Merchant Bank", 228 | "slug": "coronation-merchant-bank", 229 | "code": "559", 230 | "longcode": "", 231 | "gateway": null, 232 | "pay_with_bank": false, 233 | "active": true, 234 | "is_deleted": false, 235 | "country": "Nigeria", 236 | "currency": "NGN", 237 | "type": "nuban", 238 | "id": 173, 239 | "created_at": "2020-11-24T10:25:07.000Z", 240 | "updated_at": "2020-11-24T10:25:07.000Z" 241 | }, 242 | { 243 | "name": "Ecobank Nigeria", 244 | "slug": "ecobank-nigeria", 245 | "code": "050", 246 | "longcode": "050150010", 247 | "gateway": null, 248 | "pay_with_bank": false, 249 | "active": true, 250 | "is_deleted": null, 251 | "country": "Nigeria", 252 | "currency": "NGN", 253 | "type": "nuban", 254 | "id": 4, 255 | "created_at": "2016-07-14T10:04:29.000Z", 256 | "updated_at": "2020-02-18T20:23:53.000Z" 257 | }, 258 | { 259 | "name": "Ekondo Microfinance Bank", 260 | "slug": "ekondo-microfinance-bank", 261 | "code": "562", 262 | "longcode": "", 263 | "gateway": null, 264 | "pay_with_bank": false, 265 | "active": true, 266 | "is_deleted": null, 267 | "country": "Nigeria", 268 | "currency": "NGN", 269 | "type": "nuban", 270 | "id": 64, 271 | "created_at": "2018-09-23T05:55:06.000Z", 272 | "updated_at": "2018-09-23T05:55:06.000Z" 273 | }, 274 | { 275 | "name": "Eyowo", 276 | "slug": "eyowo", 277 | "code": "50126", 278 | "longcode": "", 279 | "gateway": null, 280 | "pay_with_bank": false, 281 | "active": true, 282 | "is_deleted": false, 283 | "country": "Nigeria", 284 | "currency": "NGN", 285 | "type": "nuban", 286 | "id": 167, 287 | "created_at": "2020-09-07T13:52:22.000Z", 288 | "updated_at": "2020-11-24T10:03:21.000Z" 289 | }, 290 | { 291 | "name": "Fidelity Bank", 292 | "slug": "fidelity-bank", 293 | "code": "070", 294 | "longcode": "070150003", 295 | "gateway": "emandate", 296 | "pay_with_bank": false, 297 | "active": true, 298 | "is_deleted": null, 299 | "country": "Nigeria", 300 | "currency": "NGN", 301 | "type": "nuban", 302 | "id": 6, 303 | "created_at": "2016-07-14T10:04:29.000Z", 304 | "updated_at": "2021-08-27T09:15:29.000Z" 305 | }, 306 | { 307 | "name": "Firmus MFB", 308 | "slug": "firmus-mfb", 309 | "code": "51314", 310 | "longcode": "", 311 | "gateway": null, 312 | "pay_with_bank": false, 313 | "active": true, 314 | "is_deleted": null, 315 | "country": "Nigeria", 316 | "currency": "NGN", 317 | "type": "nuban", 318 | "id": 177, 319 | "created_at": "2021-06-01T15:33:26.000Z", 320 | "updated_at": "2021-06-01T15:33:26.000Z" 321 | }, 322 | { 323 | "name": "First Bank of Nigeria", 324 | "slug": "first-bank-of-nigeria", 325 | "code": "011", 326 | "longcode": "011151003", 327 | "gateway": "ibank", 328 | "pay_with_bank": false, 329 | "active": true, 330 | "is_deleted": null, 331 | "country": "Nigeria", 332 | "currency": "NGN", 333 | "type": "nuban", 334 | "id": 7, 335 | "created_at": "2016-07-14T10:04:29.000Z", 336 | "updated_at": "2021-03-25T14:22:52.000Z" 337 | }, 338 | { 339 | "name": "First City Monument Bank", 340 | "slug": "first-city-monument-bank", 341 | "code": "214", 342 | "longcode": "214150018", 343 | "gateway": "emandate", 344 | "pay_with_bank": false, 345 | "active": true, 346 | "is_deleted": null, 347 | "country": "Nigeria", 348 | "currency": "NGN", 349 | "type": "nuban", 350 | "id": 8, 351 | "created_at": "2016-07-14T10:04:29.000Z", 352 | "updated_at": "2020-02-18T08:06:46.000Z" 353 | }, 354 | { 355 | "name": "FSDH Merchant Bank Limited", 356 | "slug": "fsdh-merchant-bank-limited", 357 | "code": "501", 358 | "longcode": "", 359 | "gateway": null, 360 | "pay_with_bank": false, 361 | "active": true, 362 | "is_deleted": false, 363 | "country": "Nigeria", 364 | "currency": "NGN", 365 | "type": "nuban", 366 | "id": 112, 367 | "created_at": "2020-08-20T09:37:04.000Z", 368 | "updated_at": "2020-11-24T10:03:22.000Z" 369 | }, 370 | { 371 | "name": "Gateway Mortgage Bank LTD", 372 | "slug": "gateway-mortgage-bank", 373 | "code": "812", 374 | "longcode": "", 375 | "gateway": null, 376 | "pay_with_bank": false, 377 | "active": true, 378 | "is_deleted": null, 379 | "country": "Nigeria", 380 | "currency": "NGN", 381 | "type": "nuban", 382 | "id": 287, 383 | "created_at": "2022-02-24T06:04:39.000Z", 384 | "updated_at": "2022-02-24T06:04:39.000Z" 385 | }, 386 | { 387 | "name": "Globus Bank", 388 | "slug": "globus-bank", 389 | "code": "00103", 390 | "longcode": "103015001", 391 | "gateway": null, 392 | "pay_with_bank": false, 393 | "active": true, 394 | "is_deleted": false, 395 | "country": "Nigeria", 396 | "currency": "NGN", 397 | "type": "nuban", 398 | "id": 70, 399 | "created_at": "2020-02-11T15:38:57.000Z", 400 | "updated_at": "2020-02-11T15:38:57.000Z" 401 | }, 402 | { 403 | "name": "GoMoney", 404 | "slug": "gomoney", 405 | "code": "100022", 406 | "longcode": "", 407 | "gateway": null, 408 | "pay_with_bank": false, 409 | "active": true, 410 | "is_deleted": null, 411 | "country": "Nigeria", 412 | "currency": "NGN", 413 | "type": "nuban", 414 | "id": 183, 415 | "created_at": "2021-08-04T11:49:46.000Z", 416 | "updated_at": "2021-11-12T13:32:14.000Z" 417 | }, 418 | { 419 | "name": "Guaranty Trust Bank", 420 | "slug": "guaranty-trust-bank", 421 | "code": "058", 422 | "longcode": "058152036", 423 | "gateway": "ibank", 424 | "pay_with_bank": true, 425 | "active": true, 426 | "is_deleted": null, 427 | "country": "Nigeria", 428 | "currency": "NGN", 429 | "type": "nuban", 430 | "id": 9, 431 | "created_at": "2016-07-14T10:04:29.000Z", 432 | "updated_at": "2021-01-01T11:22:11.000Z" 433 | }, 434 | { 435 | "name": "Hackman Microfinance Bank", 436 | "slug": "hackman-microfinance-bank", 437 | "code": "51251", 438 | "longcode": "", 439 | "gateway": null, 440 | "pay_with_bank": false, 441 | "active": true, 442 | "is_deleted": false, 443 | "country": "Nigeria", 444 | "currency": "NGN", 445 | "type": "nuban", 446 | "id": 111, 447 | "created_at": "2020-08-20T09:32:48.000Z", 448 | "updated_at": "2020-11-24T10:03:24.000Z" 449 | }, 450 | { 451 | "name": "Hasal Microfinance Bank", 452 | "slug": "hasal-microfinance-bank", 453 | "code": "50383", 454 | "longcode": "", 455 | "gateway": null, 456 | "pay_with_bank": false, 457 | "active": true, 458 | "is_deleted": false, 459 | "country": "Nigeria", 460 | "currency": "NGN", 461 | "type": "nuban", 462 | "id": 81, 463 | "created_at": "2020-02-11T15:38:57.000Z", 464 | "updated_at": "2020-02-11T15:38:57.000Z" 465 | }, 466 | { 467 | "name": "Heritage Bank", 468 | "slug": "heritage-bank", 469 | "code": "030", 470 | "longcode": "030159992", 471 | "gateway": null, 472 | "pay_with_bank": false, 473 | "active": true, 474 | "is_deleted": null, 475 | "country": "Nigeria", 476 | "currency": "NGN", 477 | "type": "nuban", 478 | "id": 10, 479 | "created_at": "2016-07-14T10:04:29.000Z", 480 | "updated_at": "2020-02-18T20:24:23.000Z" 481 | }, 482 | { 483 | "name": "Ibile Microfinance Bank", 484 | "slug": "ibile-mfb", 485 | "code": "51244", 486 | "longcode": "", 487 | "gateway": null, 488 | "pay_with_bank": false, 489 | "active": true, 490 | "is_deleted": false, 491 | "country": "Nigeria", 492 | "currency": "NGN", 493 | "type": "nuban", 494 | "id": 168, 495 | "created_at": "2020-10-21T10:54:20.000Z", 496 | "updated_at": "2020-10-21T10:54:33.000Z" 497 | }, 498 | { 499 | "name": "Infinity MFB", 500 | "slug": "infinity-mfb", 501 | "code": "50457", 502 | "longcode": "", 503 | "gateway": null, 504 | "pay_with_bank": false, 505 | "active": true, 506 | "is_deleted": false, 507 | "country": "Nigeria", 508 | "currency": "NGN", 509 | "type": "nuban", 510 | "id": 172, 511 | "created_at": "2020-11-24T10:23:37.000Z", 512 | "updated_at": "2020-11-24T10:23:37.000Z" 513 | }, 514 | { 515 | "name": "Jaiz Bank", 516 | "slug": "jaiz-bank", 517 | "code": "301", 518 | "longcode": "301080020", 519 | "gateway": null, 520 | "pay_with_bank": false, 521 | "active": true, 522 | "is_deleted": null, 523 | "country": "Nigeria", 524 | "currency": "NGN", 525 | "type": "nuban", 526 | "id": 22, 527 | "created_at": "2016-10-10T17:26:29.000Z", 528 | "updated_at": "2016-10-10T17:26:29.000Z" 529 | }, 530 | { 531 | "name": "Kadpoly MFB", 532 | "slug": "kadpoly-mfb", 533 | "code": "50502", 534 | "longcode": "", 535 | "gateway": null, 536 | "pay_with_bank": false, 537 | "active": true, 538 | "is_deleted": null, 539 | "country": "Nigeria", 540 | "currency": "NGN", 541 | "type": "nuban", 542 | "id": 187, 543 | "created_at": "2021-09-27T11:59:42.000Z", 544 | "updated_at": "2021-09-27T11:59:42.000Z" 545 | }, 546 | { 547 | "name": "Keystone Bank", 548 | "slug": "keystone-bank", 549 | "code": "082", 550 | "longcode": "082150017", 551 | "gateway": null, 552 | "pay_with_bank": false, 553 | "active": true, 554 | "is_deleted": null, 555 | "country": "Nigeria", 556 | "currency": "NGN", 557 | "type": "nuban", 558 | "id": 11, 559 | "created_at": "2016-07-14T10:04:29.000Z", 560 | "updated_at": "2020-02-18T20:23:45.000Z" 561 | }, 562 | { 563 | "name": "Kredi Money MFB LTD", 564 | "slug": "kredi-money-mfb", 565 | "code": "50200", 566 | "longcode": "", 567 | "gateway": null, 568 | "pay_with_bank": false, 569 | "active": true, 570 | "is_deleted": null, 571 | "country": "Nigeria", 572 | "currency": "NGN", 573 | "type": "nuban", 574 | "id": 184, 575 | "created_at": "2021-08-11T09:54:03.000Z", 576 | "updated_at": "2021-08-11T09:54:03.000Z" 577 | }, 578 | { 579 | "name": "Kuda Bank", 580 | "slug": "kuda-bank", 581 | "code": "50211", 582 | "longcode": "", 583 | "gateway": "digitalbankmandate", 584 | "pay_with_bank": true, 585 | "active": true, 586 | "is_deleted": false, 587 | "country": "Nigeria", 588 | "currency": "NGN", 589 | "type": "nuban", 590 | "id": 67, 591 | "created_at": "2019-11-15T17:06:54.000Z", 592 | "updated_at": "2020-07-01T15:05:18.000Z" 593 | }, 594 | { 595 | "name": "Lagos Building Investment Company Plc.", 596 | "slug": "lbic-plc", 597 | "code": "90052", 598 | "longcode": "", 599 | "gateway": null, 600 | "pay_with_bank": false, 601 | "active": true, 602 | "is_deleted": false, 603 | "country": "Nigeria", 604 | "currency": "NGN", 605 | "type": "nuban", 606 | "id": 109, 607 | "created_at": "2020-08-10T15:07:44.000Z", 608 | "updated_at": "2020-08-10T15:07:44.000Z" 609 | }, 610 | { 611 | "name": "Links MFB", 612 | "slug": "links-mfb", 613 | "code": "50549", 614 | "longcode": "", 615 | "gateway": null, 616 | "pay_with_bank": false, 617 | "active": true, 618 | "is_deleted": null, 619 | "country": "Nigeria", 620 | "currency": "NGN", 621 | "type": "nuban", 622 | "id": 180, 623 | "created_at": "2021-07-12T14:41:18.000Z", 624 | "updated_at": "2021-07-12T14:41:18.000Z" 625 | }, 626 | { 627 | "name": "Lotus Bank", 628 | "slug": "lotus-bank", 629 | "code": "303", 630 | "longcode": "", 631 | "gateway": null, 632 | "pay_with_bank": false, 633 | "active": true, 634 | "is_deleted": null, 635 | "country": "Nigeria", 636 | "currency": "NGN", 637 | "type": "nuban", 638 | "id": 233, 639 | "created_at": "2021-12-06T14:39:51.000Z", 640 | "updated_at": "2021-12-06T14:39:51.000Z" 641 | }, 642 | { 643 | "name": "Mayfair MFB", 644 | "slug": "mayfair-mfb", 645 | "code": "50563", 646 | "longcode": "", 647 | "gateway": null, 648 | "pay_with_bank": false, 649 | "active": true, 650 | "is_deleted": null, 651 | "country": "Nigeria", 652 | "currency": "NGN", 653 | "type": "nuban", 654 | "id": 175, 655 | "created_at": "2021-02-02T08:28:38.000Z", 656 | "updated_at": "2021-02-02T08:28:38.000Z" 657 | }, 658 | { 659 | "name": "Mint MFB", 660 | "slug": "mint-mfb", 661 | "code": "50304", 662 | "longcode": "", 663 | "gateway": null, 664 | "pay_with_bank": false, 665 | "active": true, 666 | "is_deleted": null, 667 | "country": "Nigeria", 668 | "currency": "NGN", 669 | "type": "nuban", 670 | "id": 178, 671 | "created_at": "2021-06-01T16:07:29.000Z", 672 | "updated_at": "2021-06-01T16:07:29.000Z" 673 | }, 674 | { 675 | "name": "Paga", 676 | "slug": "paga", 677 | "code": "100002", 678 | "longcode": "", 679 | "gateway": null, 680 | "pay_with_bank": false, 681 | "active": true, 682 | "is_deleted": null, 683 | "country": "Nigeria", 684 | "currency": "NGN", 685 | "type": "nuban", 686 | "id": 185, 687 | "created_at": "2021-08-31T08:10:00.000Z", 688 | "updated_at": "2021-08-31T08:10:00.000Z" 689 | }, 690 | { 691 | "name": "PalmPay", 692 | "slug": "palmpay", 693 | "code": "999991", 694 | "longcode": "", 695 | "gateway": null, 696 | "pay_with_bank": false, 697 | "active": true, 698 | "is_deleted": false, 699 | "country": "Nigeria", 700 | "currency": "NGN", 701 | "type": "nuban", 702 | "id": 169, 703 | "created_at": "2020-11-24T09:58:37.000Z", 704 | "updated_at": "2020-11-24T10:03:19.000Z" 705 | }, 706 | { 707 | "name": "Parallex Bank", 708 | "slug": "parallex-bank", 709 | "code": "104", 710 | "longcode": "", 711 | "gateway": null, 712 | "pay_with_bank": false, 713 | "active": true, 714 | "is_deleted": null, 715 | "country": "Nigeria", 716 | "currency": "NGN", 717 | "type": "nuban", 718 | "id": 26, 719 | "created_at": "2017-03-31T13:54:29.000Z", 720 | "updated_at": "2021-10-29T08:00:19.000Z" 721 | }, 722 | { 723 | "name": "Parkway - ReadyCash", 724 | "slug": "parkway-ready-cash", 725 | "code": "311", 726 | "longcode": "", 727 | "gateway": null, 728 | "pay_with_bank": false, 729 | "active": true, 730 | "is_deleted": false, 731 | "country": "Nigeria", 732 | "currency": "NGN", 733 | "type": "nuban", 734 | "id": 110, 735 | "created_at": "2020-08-10T15:07:44.000Z", 736 | "updated_at": "2020-08-10T15:07:44.000Z" 737 | }, 738 | { 739 | "name": "Paycom", 740 | "slug": "paycom", 741 | "code": "999992", 742 | "longcode": "", 743 | "gateway": null, 744 | "pay_with_bank": false, 745 | "active": true, 746 | "is_deleted": false, 747 | "country": "Nigeria", 748 | "currency": "NGN", 749 | "type": "nuban", 750 | "id": 171, 751 | "created_at": "2020-11-24T10:20:45.000Z", 752 | "updated_at": "2020-11-24T10:20:54.000Z" 753 | }, 754 | { 755 | "name": "Petra Mircofinance Bank Plc", 756 | "slug": "petra-microfinance-bank-plc", 757 | "code": "50746", 758 | "longcode": "", 759 | "gateway": null, 760 | "pay_with_bank": false, 761 | "active": true, 762 | "is_deleted": false, 763 | "country": "Nigeria", 764 | "currency": "NGN", 765 | "type": "nuban", 766 | "id": 170, 767 | "created_at": "2020-11-24T10:03:06.000Z", 768 | "updated_at": "2020-11-24T10:03:06.000Z" 769 | }, 770 | { 771 | "name": "Polaris Bank", 772 | "slug": "polaris-bank", 773 | "code": "076", 774 | "longcode": "076151006", 775 | "gateway": null, 776 | "pay_with_bank": false, 777 | "active": true, 778 | "is_deleted": null, 779 | "country": "Nigeria", 780 | "currency": "NGN", 781 | "type": "nuban", 782 | "id": 13, 783 | "created_at": "2016-07-14T10:04:29.000Z", 784 | "updated_at": "2016-07-14T10:04:29.000Z" 785 | }, 786 | { 787 | "name": "Providus Bank", 788 | "slug": "providus-bank", 789 | "code": "101", 790 | "longcode": "", 791 | "gateway": null, 792 | "pay_with_bank": false, 793 | "active": true, 794 | "is_deleted": null, 795 | "country": "Nigeria", 796 | "currency": "NGN", 797 | "type": "nuban", 798 | "id": 25, 799 | "created_at": "2017-03-27T16:09:29.000Z", 800 | "updated_at": "2021-02-09T17:50:06.000Z" 801 | }, 802 | { 803 | "name": "QuickFund MFB", 804 | "slug": "quickfund-mfb", 805 | "code": "51293", 806 | "longcode": "", 807 | "gateway": null, 808 | "pay_with_bank": false, 809 | "active": true, 810 | "is_deleted": null, 811 | "country": "Nigeria", 812 | "currency": "NGN", 813 | "type": "nuban", 814 | "id": 232, 815 | "created_at": "2021-10-29T08:43:35.000Z", 816 | "updated_at": "2021-10-29T08:43:35.000Z" 817 | }, 818 | { 819 | "name": "Rand Merchant Bank", 820 | "slug": "rand-merchant-bank", 821 | "code": "502", 822 | "longcode": "", 823 | "gateway": null, 824 | "pay_with_bank": false, 825 | "active": true, 826 | "is_deleted": null, 827 | "country": "Nigeria", 828 | "currency": "NGN", 829 | "type": "nuban", 830 | "id": 176, 831 | "created_at": "2021-02-11T17:33:20.000Z", 832 | "updated_at": "2021-02-11T17:33:20.000Z" 833 | }, 834 | { 835 | "name": "Rubies MFB", 836 | "slug": "rubies-mfb", 837 | "code": "125", 838 | "longcode": "", 839 | "gateway": null, 840 | "pay_with_bank": false, 841 | "active": true, 842 | "is_deleted": false, 843 | "country": "Nigeria", 844 | "currency": "NGN", 845 | "type": "nuban", 846 | "id": 69, 847 | "created_at": "2020-01-25T09:49:59.000Z", 848 | "updated_at": "2020-01-25T09:49:59.000Z" 849 | }, 850 | { 851 | "name": "Safe Haven MFB", 852 | "slug": "safe-haven-mfb-ng", 853 | "code": "51113", 854 | "longcode": "51113", 855 | "gateway": "", 856 | "pay_with_bank": false, 857 | "active": true, 858 | "is_deleted": false, 859 | "country": "Nigeria", 860 | "currency": "NGN", 861 | "type": "nuban", 862 | "id": 286, 863 | "created_at": "2022-02-18T13:11:59.000Z", 864 | "updated_at": "2022-02-18T13:11:59.000Z" 865 | }, 866 | { 867 | "name": "Sparkle Microfinance Bank", 868 | "slug": "sparkle-microfinance-bank", 869 | "code": "51310", 870 | "longcode": "", 871 | "gateway": null, 872 | "pay_with_bank": false, 873 | "active": true, 874 | "is_deleted": false, 875 | "country": "Nigeria", 876 | "currency": "NGN", 877 | "type": "nuban", 878 | "id": 72, 879 | "created_at": "2020-02-11T18:43:14.000Z", 880 | "updated_at": "2020-02-11T18:43:14.000Z" 881 | }, 882 | { 883 | "name": "Stanbic IBTC Bank", 884 | "slug": "stanbic-ibtc-bank", 885 | "code": "221", 886 | "longcode": "221159522", 887 | "gateway": null, 888 | "pay_with_bank": false, 889 | "active": true, 890 | "is_deleted": null, 891 | "country": "Nigeria", 892 | "currency": "NGN", 893 | "type": "nuban", 894 | "id": 14, 895 | "created_at": "2016-07-14T10:04:29.000Z", 896 | "updated_at": "2020-02-18T20:24:17.000Z" 897 | }, 898 | { 899 | "name": "Standard Chartered Bank", 900 | "slug": "standard-chartered-bank", 901 | "code": "068", 902 | "longcode": "068150015", 903 | "gateway": null, 904 | "pay_with_bank": false, 905 | "active": true, 906 | "is_deleted": null, 907 | "country": "Nigeria", 908 | "currency": "NGN", 909 | "type": "nuban", 910 | "id": 15, 911 | "created_at": "2016-07-14T10:04:29.000Z", 912 | "updated_at": "2020-02-18T20:23:40.000Z" 913 | }, 914 | { 915 | "name": "Stellas MFB", 916 | "slug": "stellas-mfb", 917 | "code": "51253", 918 | "longcode": "", 919 | "gateway": null, 920 | "pay_with_bank": false, 921 | "active": true, 922 | "is_deleted": null, 923 | "country": "Nigeria", 924 | "currency": "NGN", 925 | "type": "nuban", 926 | "id": 285, 927 | "created_at": "2022-02-17T14:54:01.000Z", 928 | "updated_at": "2022-02-17T14:54:01.000Z" 929 | }, 930 | { 931 | "name": "Sterling Bank", 932 | "slug": "sterling-bank", 933 | "code": "232", 934 | "longcode": "232150016", 935 | "gateway": "emandate", 936 | "pay_with_bank": false, 937 | "active": true, 938 | "is_deleted": null, 939 | "country": "Nigeria", 940 | "currency": "NGN", 941 | "type": "nuban", 942 | "id": 16, 943 | "created_at": "2016-07-14T10:04:29.000Z", 944 | "updated_at": "2021-11-02T20:35:10.000Z" 945 | }, 946 | { 947 | "name": "Suntrust Bank", 948 | "slug": "suntrust-bank", 949 | "code": "100", 950 | "longcode": "", 951 | "gateway": null, 952 | "pay_with_bank": false, 953 | "active": true, 954 | "is_deleted": null, 955 | "country": "Nigeria", 956 | "currency": "NGN", 957 | "type": "nuban", 958 | "id": 23, 959 | "created_at": "2016-10-10T17:26:29.000Z", 960 | "updated_at": "2016-10-10T17:26:29.000Z" 961 | }, 962 | { 963 | "name": "TAJ Bank", 964 | "slug": "taj-bank", 965 | "code": "302", 966 | "longcode": "", 967 | "gateway": null, 968 | "pay_with_bank": false, 969 | "active": true, 970 | "is_deleted": false, 971 | "country": "Nigeria", 972 | "currency": "NGN", 973 | "type": "nuban", 974 | "id": 68, 975 | "created_at": "2020-01-20T11:20:32.000Z", 976 | "updated_at": "2020-01-20T11:20:32.000Z" 977 | }, 978 | { 979 | "name": "Tangerine Money", 980 | "slug": "tangerine-money", 981 | "code": "51269", 982 | "longcode": "", 983 | "gateway": null, 984 | "pay_with_bank": false, 985 | "active": true, 986 | "is_deleted": null, 987 | "country": "Nigeria", 988 | "currency": "NGN", 989 | "type": "nuban", 990 | "id": 186, 991 | "created_at": "2021-09-17T13:25:16.000Z", 992 | "updated_at": "2021-09-17T13:25:16.000Z" 993 | }, 994 | { 995 | "name": "TCF MFB", 996 | "slug": "tcf-mfb", 997 | "code": "51211", 998 | "longcode": "", 999 | "gateway": null, 1000 | "pay_with_bank": false, 1001 | "active": true, 1002 | "is_deleted": false, 1003 | "country": "Nigeria", 1004 | "currency": "NGN", 1005 | "type": "nuban", 1006 | "id": 75, 1007 | "created_at": "2020-04-03T09:34:35.000Z", 1008 | "updated_at": "2020-04-03T09:34:35.000Z" 1009 | }, 1010 | { 1011 | "name": "Titan Bank", 1012 | "slug": "titan-bank", 1013 | "code": "102", 1014 | "longcode": "", 1015 | "gateway": null, 1016 | "pay_with_bank": false, 1017 | "active": true, 1018 | "is_deleted": false, 1019 | "country": "Nigeria", 1020 | "currency": "NGN", 1021 | "type": "nuban", 1022 | "id": 73, 1023 | "created_at": "2020-03-10T11:41:36.000Z", 1024 | "updated_at": "2020-03-23T15:06:29.000Z" 1025 | }, 1026 | { 1027 | "name": "Unical MFB", 1028 | "slug": "unical-mfb", 1029 | "code": "50871", 1030 | "longcode": "", 1031 | "gateway": null, 1032 | "pay_with_bank": false, 1033 | "active": true, 1034 | "is_deleted": null, 1035 | "country": "Nigeria", 1036 | "currency": "NGN", 1037 | "type": "nuban", 1038 | "id": 282, 1039 | "created_at": "2022-01-10T09:52:47.000Z", 1040 | "updated_at": "2022-01-10T09:52:47.000Z" 1041 | }, 1042 | { 1043 | "name": "Union Bank of Nigeria", 1044 | "slug": "union-bank-of-nigeria", 1045 | "code": "032", 1046 | "longcode": "032080474", 1047 | "gateway": "emandate", 1048 | "pay_with_bank": false, 1049 | "active": true, 1050 | "is_deleted": null, 1051 | "country": "Nigeria", 1052 | "currency": "NGN", 1053 | "type": "nuban", 1054 | "id": 17, 1055 | "created_at": "2016-07-14T10:04:29.000Z", 1056 | "updated_at": "2020-02-18T20:22:54.000Z" 1057 | }, 1058 | { 1059 | "name": "United Bank For Africa", 1060 | "slug": "united-bank-for-africa", 1061 | "code": "033", 1062 | "longcode": "033153513", 1063 | "gateway": "emandate", 1064 | "pay_with_bank": false, 1065 | "active": true, 1066 | "is_deleted": null, 1067 | "country": "Nigeria", 1068 | "currency": "NGN", 1069 | "type": "nuban", 1070 | "id": 18, 1071 | "created_at": "2016-07-14T10:04:29.000Z", 1072 | "updated_at": "2022-03-09T10:28:57.000Z" 1073 | }, 1074 | { 1075 | "name": "Unity Bank", 1076 | "slug": "unity-bank", 1077 | "code": "215", 1078 | "longcode": "215154097", 1079 | "gateway": "emandate", 1080 | "pay_with_bank": false, 1081 | "active": true, 1082 | "is_deleted": null, 1083 | "country": "Nigeria", 1084 | "currency": "NGN", 1085 | "type": "nuban", 1086 | "id": 19, 1087 | "created_at": "2016-07-14T10:04:29.000Z", 1088 | "updated_at": "2019-07-22T12:44:02.000Z" 1089 | }, 1090 | { 1091 | "name": "VFD Microfinance Bank Limited", 1092 | "slug": "vfd", 1093 | "code": "566", 1094 | "longcode": "", 1095 | "gateway": null, 1096 | "pay_with_bank": false, 1097 | "active": true, 1098 | "is_deleted": false, 1099 | "country": "Nigeria", 1100 | "currency": "NGN", 1101 | "type": "nuban", 1102 | "id": 71, 1103 | "created_at": "2020-02-11T15:44:11.000Z", 1104 | "updated_at": "2020-10-28T09:42:08.000Z" 1105 | }, 1106 | { 1107 | "name": "Wema Bank", 1108 | "slug": "wema-bank", 1109 | "code": "035", 1110 | "longcode": "035150103", 1111 | "gateway": null, 1112 | "pay_with_bank": false, 1113 | "active": true, 1114 | "is_deleted": null, 1115 | "country": "Nigeria", 1116 | "currency": "NGN", 1117 | "type": "nuban", 1118 | "id": 20, 1119 | "created_at": "2016-07-14T10:04:29.000Z", 1120 | "updated_at": "2021-02-09T17:49:59.000Z" 1121 | }, 1122 | { 1123 | "name": "Zenith Bank", 1124 | "slug": "zenith-bank", 1125 | "code": "057", 1126 | "longcode": "057150013", 1127 | "gateway": "emandate", 1128 | "pay_with_bank": false, 1129 | "active": true, 1130 | "is_deleted": null, 1131 | "country": "Nigeria", 1132 | "currency": "NGN", 1133 | "type": "nuban", 1134 | "id": 21, 1135 | "created_at": "2016-07-14T10:04:29.000Z", 1136 | "updated_at": "2022-03-16T10:15:29.000Z" 1137 | }, 1138 | { 1139 | "name": "Zenith Bank", 1140 | "slug": "zenith-bank-usd", 1141 | "code": "057", 1142 | "longcode": "057150013", 1143 | "gateway": null, 1144 | "pay_with_bank": false, 1145 | "active": true, 1146 | "is_deleted": null, 1147 | "country": "Nigeria", 1148 | "currency": "USD", 1149 | "type": "nuban", 1150 | "id": 65, 1151 | "created_at": null, 1152 | "updated_at": "2022-03-16T10:15:29.000Z" 1153 | } 1154 | ] -------------------------------------------------------------------------------- /helpers/payment.helpers.js: -------------------------------------------------------------------------------- 1 | require("dotenv/config"); 2 | const randomstring = require("randomstring"); 3 | const FlutterwaveKey = process.env.FLUTTERWAVE_KEY; 4 | const axios = require("axios"); 5 | 6 | /** 7 | * Make Payment with flutterwave 8 | * 9 | * @param {Integer} amount 10 | * @param {Object} authenticatedUser 11 | * @param {String} redirect_url 12 | * @param {String} description 13 | * @returns {String} 14 | */ 15 | const makePayment = async (amount, authenticatedUser, redirect_url, description) => { 16 | try { 17 | const generatedTransactionReference = randomstring.generate({ 18 | length: 10, 19 | charset: "alphanumeric", 20 | capitalization: "uppercase", 21 | }); 22 | const paymentLink = await axios({ 23 | method: "post", 24 | url: "https://api.flutterwave.com/v3/payments", 25 | data: { 26 | tx_ref: `PID-${generatedTransactionReference}`, 27 | amount: amount, 28 | currency: "NGN", 29 | redirect_url: redirect_url, 30 | payment_options: "card", 31 | customer: { 32 | email: authenticatedUser.email, 33 | name: authenticatedUser.first_name + " " + authenticatedUser.last_name, 34 | }, 35 | customizations: { 36 | title: "E-wallet", 37 | description: description, 38 | }, 39 | }, 40 | headers: { 41 | Authorization: `Bearer ${FlutterwaveKey}`, 42 | Accept: "application/json", 43 | }, 44 | }); 45 | return paymentLink.data.data.link; 46 | } catch (error) { 47 | console.error("MakePayment Error>>", error.message); 48 | throw new Error(error); 49 | } 50 | }; 51 | 52 | /** 53 | * Verify Payment with flutterwave 54 | * 55 | * @param {Integer} transactionId 56 | * @returns {Object} 57 | */ 58 | 59 | const verifyPayment = async (transactionId) => { 60 | try { 61 | const paymentVerification = await axios({ 62 | method: "get", 63 | url: `https://api.flutterwave.com/v3/transactions/${transactionId}/verify`, 64 | headers: { 65 | Authorization: `Bearer ${FlutterwaveKey}`, 66 | Accept: "application/json", 67 | }, 68 | }); 69 | return paymentVerification.data.data; 70 | } catch (error) { 71 | console.error("VerifyPayment Error>>", error.message); 72 | throw new Error(error); 73 | } 74 | }; 75 | 76 | /** 77 | * Widthraw Payment with flutterwave 78 | * 79 | * @param {Integer} amount 80 | * @param {String} bank_code 81 | * @param {String} account_number 82 | * @returns {Object} 83 | */ 84 | 85 | const withdrawPayment = async (amount, bank_code, account_number) => { 86 | try { 87 | /** 88 | * NOTE: 89 | * Withdraw Fund does work because: Compliance approval required to use this feature 90 | */ 91 | 92 | const generatedTransactionReference = randomstring.generate({ 93 | length: 10, 94 | charset: "alphanumeric", 95 | capitalization: "uppercase", 96 | }); 97 | 98 | const mockWithdrawFundResponse = { 99 | status: "success", 100 | message: "Transfer Queued Successfully", 101 | data: { 102 | id: 190626, 103 | account_number: "0690000040", 104 | bank_code: "044", 105 | full_name: "Alexis Sanchez", 106 | created_at: "2021-04-26T11:19:55.000Z", 107 | currency: "NGN", 108 | debit_currency: "NGN", 109 | amount: amount, 110 | fee: 10.75, 111 | status: "NEW", 112 | reference: `PID-${generatedTransactionReference}`, 113 | meta: null, 114 | narration: "Payment for things", 115 | complete_message: "", 116 | requires_approval: 0, 117 | is_approved: 1, 118 | bank_name: "ACCESS BANK NIGERIA", 119 | }, 120 | }; 121 | return mockWithdrawFundResponse.data; 122 | } catch (error) { 123 | console.error("withdrawPayment Error>>", error); 124 | throw new Error(error); 125 | } 126 | }; 127 | 128 | module.exports = { makePayment, verifyPayment, withdrawPayment }; 129 | -------------------------------------------------------------------------------- /knexfile.js: -------------------------------------------------------------------------------- 1 | // Update with your config settings. 2 | require('dotenv/config'); 3 | const Url = require('url-parse'); 4 | const CLEARDB_DATABASE_URL = new Url(process.env.CLEARDB_DATABASE_URL); 5 | 6 | /** 7 | * @type { Object. } 8 | */ 9 | module.exports = { 10 | 11 | development: { 12 | client: 'mysql2', 13 | connection: { 14 | database: process.env.DB_NAME, 15 | user: process.env.DB_USER, 16 | password: process.env.DB_PASSWORD, 17 | host: process.env.DB_HOST, 18 | port: process.env.DB_PORT 19 | }, 20 | pool: { 21 | min: 2, 22 | max: 10 23 | }, 24 | migrations: { 25 | tableName: 'knex_migrations' 26 | } 27 | }, 28 | 29 | test: { 30 | client: 'mysql2', 31 | connection: { 32 | database: process.env.TEST_DB_NAME, 33 | user: process.env.TEST_DB_USER, 34 | password: process.env.TEST_DB_PASSWORD, 35 | host: process.env.DB_HOST, 36 | port: process.env.TEST_DB_PORT 37 | }, 38 | pool: { 39 | min: 2, 40 | max: 10 41 | }, 42 | migrations: { 43 | tableName: 'knex_migrations' 44 | } 45 | }, 46 | 47 | staging: { 48 | client: 'mysql2', 49 | connection: { 50 | database: CLEARDB_DATABASE_URL.pathname.substring(1), 51 | user: CLEARDB_DATABASE_URL.username, 52 | password: CLEARDB_DATABASE_URL.password, 53 | host: CLEARDB_DATABASE_URL.host, 54 | ssl: { 55 | rejectUnauthorized: true, 56 | } 57 | }, 58 | pool: { 59 | min: 2, 60 | max: 10 61 | }, 62 | migrations: { 63 | tableName: 'knex_migrations' 64 | } 65 | }, 66 | 67 | production: { 68 | client: 'mysql2', 69 | connection: { 70 | database: CLEARDB_DATABASE_URL.pathname.substring(1), 71 | user: CLEARDB_DATABASE_URL.username, 72 | password: CLEARDB_DATABASE_URL.password, 73 | host: CLEARDB_DATABASE_URL.host, 74 | ssl: { 75 | rejectUnauthorized: true, 76 | } 77 | }, 78 | pool: { 79 | min: 2, 80 | max: 10 81 | }, 82 | migrations: { 83 | tableName: 'knex_migrations' 84 | } 85 | } 86 | 87 | }; 88 | -------------------------------------------------------------------------------- /middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const httpStatus = require("http-status"); 3 | const jwtConfig = require("../config/jwt"); 4 | 5 | const auth = async (req, res, next) => { 6 | try { 7 | let token = req.headers["authorization"]; 8 | if (!token) { 9 | return res.status(httpStatus.UNAUTHORIZED).send({ 10 | success: false, 11 | message: "This resources requires authorization", 12 | }); 13 | } 14 | const decodeToken = await jwt.verify(token.split(' ')[1], jwtConfig.appKey) 15 | req.user = decodeToken 16 | next(); 17 | } catch (error) { 18 | console.error("Auth Middleware Error ==>", error); 19 | return res.status(httpStatus.INTERNAL_SERVER_ERROR).send(error); 20 | } 21 | 22 | }; 23 | 24 | module.exports = { 25 | auth 26 | } -------------------------------------------------------------------------------- /middlewares/error-handler.js: -------------------------------------------------------------------------------- 1 | const httpStatus = require("http-status"); 2 | 3 | const sendErrorDev = (err, req, res) => { 4 | // Development error handling - send the full error stack trace 5 | res.status(err.statusCode).json({ 6 | success: false, 7 | error: err, 8 | message: err.message, 9 | stack: err.stack, 10 | }); 11 | }; 12 | 13 | const sendErrorProd = (err, req, res) => { 14 | // Production error handling - send a simpler error message without the stack trace 15 | console.error("ERROR", err); 16 | res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ 17 | success: false, 18 | message: httpStatus[httpStatus.INTERNAL_SERVER_ERROR], 19 | }); 20 | }; 21 | 22 | const errorHandler = (err, req, res, next) => { 23 | err.statusCode = err.statusCode || 500; 24 | 25 | if (err.isOperational) { 26 | res.status(err.statusCode).json({ 27 | success: false, 28 | message: err.message, 29 | }); 30 | } else { 31 | if (process.env.NODE_ENV === "production") { 32 | let error = { ...err }; 33 | error.message = err.message; 34 | sendErrorProd(error, req, res); 35 | } else { 36 | sendErrorDev(err, req, res); 37 | } 38 | } 39 | }; 40 | 41 | module.exports = errorHandler; 42 | -------------------------------------------------------------------------------- /middlewares/set-wallet-pin.js: -------------------------------------------------------------------------------- 1 | const httpStatus = require("http-status"); 2 | const db = require("../config/db"); 3 | 4 | 5 | const setWalletPin = async (req, res, next) => { 6 | try { 7 | const user = req.user; 8 | 9 | const wallet = await db("wallets").where("user_id", user.id).first(); 10 | 11 | if (!wallet.wallet_pin) { 12 | return res.status(httpStatus.BAD_REQUEST).send({ 13 | success: false, 14 | message: "Please set your wallet pin before performing any transaction", 15 | }); 16 | } 17 | next(); 18 | } catch (error) { 19 | console.error("setWalletPin Middleware Error ==>", error); 20 | return res.status(httpStatus.INTERNAL_SERVER_ERROR).send(error); 21 | } 22 | }; 23 | 24 | module.exports = { 25 | setWalletPin, 26 | }; 27 | -------------------------------------------------------------------------------- /migrations/20220226055100_users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | exports.up = function (knex) { 6 | return knex.schema.createTable("users", function (table) { 7 | table.increments("id").primary(); 8 | table.string("first_name").notNullable(); 9 | table.string("last_name").notNullable(); 10 | table.string("email").unique().notNullable(); 11 | table.string("password").notNullable(); 12 | table.timestamps(true, true); 13 | }); 14 | }; 15 | 16 | /** 17 | * @param { import("knex").Knex } knex 18 | * @returns { Promise } 19 | */ 20 | exports.down = function (knex) { 21 | return knex.schema.dropTable("users"); 22 | }; 23 | -------------------------------------------------------------------------------- /migrations/20220226091212_wallets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | exports.up = function (knex) { 6 | return knex.schema.createTable("wallets", function (table) { 7 | table.increments("id").primary(); 8 | table.integer("user_id").notNullable(); 9 | table.string("wallet_code").notNullable(); 10 | table.string("wallet_pin").defaultTo(null); 11 | table.decimal("balance", 12, 2).defaultTo(0); 12 | table.timestamps(true, true); 13 | }); 14 | }; 15 | 16 | /** 17 | * @param { import("knex").Knex } knex 18 | * @returns { Promise } 19 | */ 20 | exports.down = function (knex) { 21 | return knex.schema.dropTable("wallets"); 22 | }; 23 | -------------------------------------------------------------------------------- /migrations/20220227082352_transactions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | exports.up = function(knex) { 6 | return knex.schema.createTable("transactions", function (table) { 7 | table.increments("id").primary(); 8 | table.integer("user_id").notNullable(); 9 | table.string("transaction_code").notNullable(); 10 | table.string("transaction_reference").notNullable(); 11 | table.decimal("amount", 12, 2).notNullable(); 12 | table.string("description").notNullable(); 13 | table.string("status", 80).notNullable(); 14 | table.string("payment_method").notNullable(); 15 | table.boolean("is_inflow").defaultTo(null); 16 | table.timestamps(true, true); 17 | }); 18 | }; 19 | 20 | /** 21 | * @param { import("knex").Knex } knex 22 | * @returns { Promise } 23 | */ 24 | exports.down = function(knex) { 25 | return knex.schema.dropTable("transactions"); 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e-wallet-system", 3 | "version": "1.0.0", 4 | "description": "This system allow users to fund their account, transfer funds and withdraw from their account.", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "migrate": "knex migrate:latest", 9 | "migrate:reset": "knex migrate:rollback && npm run migrate", 10 | "test": "cross-env NODE_ENV=test node --experimental-vm-modules node_modules/jest/bin/jest.js jest --coverage --testPathPattern=tests --testTimeout=10000 --runInBand --detectOpenHandles --forceExit", 11 | "pretest": "cross-env NODE_ENV=test npm run migrate:reset" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/devwalex/e-wallet-system.git" 16 | }, 17 | "keywords": [], 18 | "author": "Usman Salami", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/devwalex/e-wallet-system/issues" 22 | }, 23 | "homepage": "https://github.com/devwalex/e-wallet-system#readme", 24 | "dependencies": { 25 | "axios": "^0.26.0", 26 | "bcryptjs": "^2.4.3", 27 | "compression": "^1.7.4", 28 | "cors": "^2.8.5", 29 | "dotenv": "^8.2.0", 30 | "express": "^4.17.3", 31 | "express-validator": "^6.14.0", 32 | "helmet": "^3.21.2", 33 | "http-status": "^1.4.0", 34 | "joi": "^17.6.0", 35 | "jsonwebtoken": "^8.5.1", 36 | "knex": "^1.0.3", 37 | "knex-paginate": "^3.0.1", 38 | "mysql2": "^3.2.0", 39 | "randomstring": "^1.2.2", 40 | "url-parse": "^1.5.10", 41 | "validator": "^13.0.0", 42 | "xss-clean": "^0.1.1" 43 | }, 44 | "devDependencies": { 45 | "axios-mock-adapter": "^1.20.0", 46 | "cross-env": "^7.0.3", 47 | "jest": "^27.5.1", 48 | "nodemon": "^2.0.15", 49 | "supertest": "^6.2.2" 50 | }, 51 | "jest": { 52 | "testEnvironment": "node", 53 | "coveragePathIgnorePatterns": [ 54 | "/node_modules/" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /release-tasks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running Release Tasks" 4 | 5 | echo "Running Migrations" 6 | ENV_SILENT=true npm run migrate 7 | 8 | echo "Refreshing Migrations" 9 | #ENV_SILENT=true npm run migrate:reset 10 | 11 | echo "Done" -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const userRoute = require('./user.route'); 3 | const walletRoute = require('./wallet.route'); 4 | const transactionRoute = require('./transaction.route'); 5 | const router = express.Router(); 6 | 7 | router.use(userRoute); 8 | router.use(walletRoute); 9 | router.use(transactionRoute); 10 | 11 | router.get('/', (req, res) => { 12 | return res.status(200).json({ message: 'E-Wallet System' }); 13 | }); 14 | 15 | module.exports = router; -------------------------------------------------------------------------------- /routes/transaction.route.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const transactionController = require("../controllers/transaction.controller"); 3 | const { auth } = require("../middlewares/auth"); 4 | 5 | const router = express.Router(); 6 | router.get("/transactions", [auth], transactionController.getTransactions); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /routes/user.route.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const userController = require("../controllers/user.controller"); 3 | const { userValidation } = require("../validations"); 4 | const { auth } = require("../middlewares/auth"); 5 | 6 | const router = express.Router(); 7 | 8 | router.post("/register", userValidation.register, userController.register); 9 | router.post("/login", userValidation.login, userController.login); 10 | router.get("/auth/profile", [auth], userController.getProfile); 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /routes/wallet.route.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const walletController = require("../controllers/wallet.controller"); 3 | const { walletValidation } = require('../validations'); 4 | const { auth } = require("../middlewares/auth"); 5 | const { setWalletPin } = require("../middlewares/set-wallet-pin"); 6 | 7 | const router = express.Router(); 8 | 9 | router.post("/wallet/set-pin", [auth, walletValidation.setWalletPin], walletController.setWalletPin); 10 | router.post("/wallet/fund", [auth, setWalletPin, walletValidation.fundWallet], walletController.fundWallet); 11 | router.get("/wallet/verify", [auth, setWalletPin], walletController.verifyWalletFunding); 12 | router.post("/wallet/transfer", [auth, setWalletPin, walletValidation.transferFund], walletController.transferFund); 13 | router.post("/wallet/withdraw", [auth, setWalletPin, walletValidation.withdrawFund], walletController.withdrawFund); 14 | router.get("/wallet/balance", [auth, setWalletPin], walletController.getWalletBalance); 15 | router.get("/wallet/banks", [auth, setWalletPin], walletController.getBanks); 16 | 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const app = require("./app"); 2 | 3 | const PORT = process.env.PORT || "3000"; 4 | let server = app.listen(PORT, () => console.log(`Server is running on port ${PORT}`)); 5 | 6 | const closeServer = () => { 7 | // close the server and exit the process 8 | server.close(() => { 9 | process.exit(1); 10 | }); 11 | }; 12 | 13 | // Handle uncaught exceptions 14 | process.on("uncaughtException", (err) => { 15 | console.error("Uncaught exception:", err); 16 | closeServer(); 17 | }); 18 | 19 | // Handle unhandled rejections 20 | process.on("unhandledRejection", (err) => { 21 | console.error("Unhandled rejection:", err); 22 | closeServer(); 23 | }); 24 | 25 | // Graceful shutdown 26 | process.on("SIGTERM", () => { 27 | console.log("Received SIGTERM. Shutting down gracefully..."); 28 | server.close(() => { 29 | console.log("HTTP server closed."); 30 | process.exit(0); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /services/index.js: -------------------------------------------------------------------------------- 1 | module.exports.userService = require('./user.service'); 2 | module.exports.walletService = require('./wallet.service'); 3 | module.exports.transactionService = require('./transaction.service'); -------------------------------------------------------------------------------- /services/transaction.service.js: -------------------------------------------------------------------------------- 1 | const db = require('../config/db') 2 | 3 | /** 4 | * Get Transactions 5 | * @param {Object} transactionData 6 | * @returns {Promise} 7 | */ 8 | 9 | const getTransactions = async (transactionData) => { 10 | const transactions = await db("transactions").where("user_id", transactionData.userId).orderBy("id", "desc").paginate({ perPage: transactionData.limit, currentPage: transactionData.page, isLengthAware: true }); 11 | return transactions; 12 | }; 13 | 14 | module.exports = { 15 | getTransactions, 16 | }; -------------------------------------------------------------------------------- /services/user.service.js: -------------------------------------------------------------------------------- 1 | const db = require('../config/db') 2 | const bcrypt = require('bcryptjs') 3 | 4 | /** 5 | * Create a new User 6 | * @param {Object} userData 7 | * @returns {Promise} 8 | */ 9 | 10 | const createUser = async(userData) => { 11 | 12 | const {first_name, last_name, email, password } = userData 13 | 14 | const hashPassword = await bcrypt.hashSync(password, 10) 15 | 16 | const user = await db('users').insert( {first_name, last_name, email, password: hashPassword }) 17 | return user 18 | } 19 | 20 | /** 21 | * Find User By Email 22 | * @param {String} email 23 | * @returns {Promise} 24 | */ 25 | 26 | const findUserByEmail = async(email) => { 27 | const user = await db.select('*').from('users').where('email', email).first() 28 | return user 29 | } 30 | 31 | /** 32 | * Get Profile 33 | * @param {Object} userData 34 | * @returns {Promise} 35 | */ 36 | 37 | const getProfile = async(userData) => { 38 | const user = await findUserByEmail(userData.email) 39 | delete user.password 40 | return user 41 | } 42 | 43 | 44 | module.exports = { 45 | createUser, 46 | findUserByEmail, 47 | getProfile 48 | }; -------------------------------------------------------------------------------- /services/wallet.service.js: -------------------------------------------------------------------------------- 1 | const db = require("../config/db"); 2 | const randomstring = require("randomstring"); 3 | const bcrypt = require("bcryptjs"); 4 | const { makePayment, verifyPayment, withdrawPayment } = require("../helpers/payment.helpers"); 5 | require("dotenv/config"); 6 | const banks = require("../helpers/json/banks.json"); 7 | const NotFoundError = require("../utils/errors/notfound.error"); 8 | const BadRequestError = require("../utils/errors/badrequest.error"); 9 | 10 | /** 11 | * Create Wallet 12 | * @param {Integer} userID 13 | * @returns {Promise} 14 | */ 15 | 16 | const createWallet = async (userID) => { 17 | const user = await db.select("*").from("users").where("id", userID).first(); 18 | 19 | const generatedWalletCode = randomstring.generate({ 20 | length: 7, 21 | charset: "alphanumeric", 22 | capitalization: "uppercase", 23 | }); 24 | 25 | const wallet = await db("wallets").insert({ 26 | user_id: user.id, 27 | wallet_code: generatedWalletCode, 28 | }); 29 | return wallet; 30 | }; 31 | 32 | /** 33 | * Set Wallet Pin 34 | * @param {Object} walletData 35 | * @returns {Promise} 36 | */ 37 | 38 | const setWalletPin = async (walletData) => { 39 | const user = walletData.user; 40 | const pin = walletData.pin.toString(); 41 | 42 | const hashPin = await bcrypt.hashSync(pin, 10); 43 | 44 | const wallet = await db("wallets").where("user_id", user.id).first(); 45 | if (!wallet.wallet_pin) { 46 | await db("wallets").where("user_id", user.id).update({ wallet_pin: hashPin }); 47 | } 48 | return wallet; 49 | }; 50 | 51 | /** 52 | * Fund Wallet 53 | * @param {Object} walletData 54 | * @returns {String} paymentLink 55 | */ 56 | 57 | const fundWallet = async (walletData) => { 58 | const user = walletData.user; 59 | const amount = walletData.amount; 60 | const frontendBaseUrl = walletData.frontend_base_url; 61 | 62 | let appUrl; 63 | if (!frontendBaseUrl) { 64 | appUrl = process.env.APP_URL ? process.env.APP_URL : "http://localhost:3000"; 65 | } else { 66 | appUrl = frontendBaseUrl; 67 | } 68 | 69 | return makePayment(amount, user, `${appUrl}/wallet/verify`, "Wallet Funding"); 70 | }; 71 | 72 | /** 73 | * Verify Wallet Funding 74 | * @param {Object} walletData 75 | * @returns {Promise} 76 | */ 77 | 78 | const verifyWalletFunding = async (walletData) => { 79 | const user = walletData.user; 80 | 81 | const payment = await verifyPayment(walletData.transaction_id); 82 | 83 | if (payment.customer.email !== user.email) { 84 | return Promise.reject({ 85 | success: false, 86 | message: "Could not verify payment", 87 | }); 88 | } 89 | await db.transaction(async (trx) => { 90 | const transaction = await trx("transactions").where("user_id", user.id).where("transaction_code", payment.id).first(); 91 | 92 | if (!transaction) { 93 | await trx("wallets").where("user_id", user.id).increment("balance", payment.amount); 94 | 95 | await trx("transactions").insert({ 96 | user_id: user.id, 97 | transaction_code: payment.id, 98 | transaction_reference: payment.tx_ref, 99 | amount: payment.amount, 100 | description: "Wallet Funding", 101 | status: payment.status, 102 | payment_method: payment.payment_type, 103 | is_inflow: true, 104 | }); 105 | } 106 | }); 107 | return payment; 108 | }; 109 | 110 | /** 111 | * Transfer Fund 112 | * @param {Object} walletData 113 | * @returns {Promise} 114 | */ 115 | 116 | const transferFund = async (walletData) => { 117 | const sender = walletData.user; 118 | const walletCodeOrEmail = walletData.wallet_code_or_email; 119 | const amount = walletData.amount; 120 | const walletPin = walletData.wallet_pin; 121 | 122 | let recipient; 123 | if (walletCodeOrEmail.includes("@")) { 124 | recipient = await db("users").where("email", walletCodeOrEmail).first(); 125 | } else { 126 | const recipientWallet = await db("wallets").where("wallet_code", walletCodeOrEmail).first(); 127 | 128 | const recipientID = recipientWallet?.user_id || null; 129 | 130 | recipient = await db("users").where("id", recipientID).first(); 131 | } 132 | 133 | if (!recipient) { 134 | throw new NotFoundError("Recipient not found"); 135 | } 136 | 137 | if (sender.id === recipient.id) { 138 | throw new BadRequestError("You cannot transfer fund to yourself"); 139 | } 140 | 141 | const senderWallet = await db("wallets").where("user_id", sender.id).first(); 142 | 143 | if (amount == 0) { 144 | throw new BadRequestError("Invalid Amount"); 145 | } 146 | 147 | if (senderWallet.balance < amount) { 148 | throw new BadRequestError("Insufficient Fund"); 149 | } 150 | 151 | // Check if wallet pin is correct 152 | const match = await bcrypt.compare(walletPin, senderWallet.wallet_pin); 153 | 154 | if (!match) { 155 | throw new BadRequestError("Incorrect Pin"); 156 | } 157 | 158 | const generatedTransactionReference = randomstring.generate({ 159 | length: 10, 160 | charset: "alphanumeric", 161 | capitalization: "uppercase", 162 | }); 163 | 164 | const generatedTransactionCode = randomstring.generate({ 165 | length: 7, 166 | charset: "numeric", 167 | }); 168 | 169 | await db.transaction(async (trx) => { 170 | // Deduct from sender wallet 171 | await trx("wallets").where("user_id", sender.id).decrement("balance", amount); 172 | // save the transaction 173 | await trx("transactions").insert({ 174 | user_id: sender.id, 175 | transaction_code: generatedTransactionCode, 176 | transaction_reference: `PID-${generatedTransactionReference}`, 177 | amount: amount, 178 | description: "Fund Transfer", 179 | status: "successful", 180 | payment_method: "wallet", 181 | is_inflow: false, 182 | }); 183 | 184 | // Add to recipient wallet 185 | await trx("wallets").where("user_id", recipient.id).increment("balance", amount); 186 | // save the transaction 187 | await trx("transactions").insert({ 188 | user_id: recipient.id, 189 | transaction_code: generatedTransactionCode, 190 | transaction_reference: `PID-${generatedTransactionReference}`, 191 | amount: amount, 192 | description: "Fund Transfer", 193 | status: "successful", 194 | payment_method: "wallet", 195 | is_inflow: true, 196 | }); 197 | }); 198 | }; 199 | 200 | /** 201 | * Withdraw Fund 202 | * @param {Object} walletData 203 | * @returns {Promise} 204 | */ 205 | 206 | const withdrawFund = async (walletData) => { 207 | const user = walletData.user; 208 | const bankCode = walletData.bank_code; 209 | const accountNumber = walletData.account_number; 210 | const amount = walletData.amount; 211 | const walletPin = walletData.wallet_pin; 212 | 213 | const userWallet = await db("wallets").where("user_id", user.id).first(); 214 | 215 | if (userWallet.balance < amount) { 216 | throw new BadRequestError("Insufficient Fund"); 217 | } 218 | 219 | // Check if wallet pin is correct 220 | const match = await bcrypt.compare(walletPin, userWallet.wallet_pin); 221 | 222 | if (!match) { 223 | throw new BadRequestError("Incorrect Pin"); 224 | } 225 | 226 | const payment = await withdrawPayment(amount, bankCode, accountNumber); 227 | 228 | const amountToDeduct = payment.amount + payment.fee; 229 | 230 | await db.transaction(async (trx) => { 231 | // Deduct from user wallet 232 | await trx("wallets").where("user_id", user.id).decrement("balance", amountToDeduct); 233 | 234 | // save the transaction for the amount withdrew 235 | await trx("transactions").insert({ 236 | user_id: user.id, 237 | transaction_code: payment.id, 238 | transaction_reference: payment.reference, 239 | amount: payment.amount, 240 | description: "Fund Withdrawal", 241 | status: "successful", 242 | payment_method: "bank transfer", 243 | is_inflow: false, 244 | }); 245 | 246 | // save the transaction for the fee of amount withdrew 247 | await trx("transactions").insert({ 248 | user_id: user.id, 249 | transaction_code: payment.id, 250 | transaction_reference: payment.reference, 251 | amount: payment.fee, 252 | description: "Fund Withdrawal Fee", 253 | status: "successful", 254 | payment_method: "bank transfer", 255 | is_inflow: false, 256 | }); 257 | }); 258 | }; 259 | 260 | /** 261 | * Get Wallet Balance 262 | * @param {Object} walletData 263 | * @returns {Promise} 264 | */ 265 | 266 | const getWalletBalance = async (walletData) => { 267 | const user = walletData.user; 268 | const wallet = await db("wallets").where("user_id", user.id).first(); 269 | 270 | return wallet; 271 | }; 272 | 273 | /** 274 | * Get Banks 275 | * @returns {Array} banks - array of banks 276 | */ 277 | 278 | const getBanks = () => { 279 | return banks; 280 | }; 281 | 282 | module.exports = { 283 | createWallet, 284 | setWalletPin, 285 | fundWallet, 286 | verifyWalletFunding, 287 | transferFund, 288 | withdrawFund, 289 | getWalletBalance, 290 | getBanks, 291 | }; 292 | -------------------------------------------------------------------------------- /start-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "running migrations....." 3 | npm run migrate 4 | echo "done" 5 | 6 | echo "starting server....." 7 | npm start 8 | -------------------------------------------------------------------------------- /tests/transaction.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../app"); 3 | 4 | let token; 5 | beforeAll(async () => { 6 | await request(app).post("/register").send({ 7 | first_name: "Carter", 8 | last_name: "Doe", 9 | email: "carter@gmail.com", 10 | password: "123456", 11 | }); 12 | 13 | const response = await request(app).post("/login").send({ 14 | email: "carter@gmail.com", 15 | password: "123456", 16 | }); 17 | token = response.body.token; 18 | }); 19 | 20 | describe("Transaction", () => { 21 | it("should get all transactions successfully", async () => { 22 | const response = await request(app) 23 | .get("/transactions") 24 | .set({ Authorization: `Bearer ${token}` }) 25 | .send(); 26 | expect(response.statusCode).toEqual(200); 27 | expect(response.body.message).toEqual("Returned transactions successfully"); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/user.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../app"); 3 | 4 | let token; 5 | beforeAll(async () => { 6 | await request(app).post("/register").send({ 7 | first_name: "Carter", 8 | last_name: "Doe", 9 | email: "carter@gmail.com", 10 | password: "123456", 11 | }); 12 | 13 | const response = await request(app).post("/login").send({ 14 | email: "carter@gmail.com", 15 | password: "123456", 16 | }); 17 | token = response.body.token; 18 | }); 19 | 20 | describe("User", () => { 21 | it("should register a user successfully", async () => { 22 | const response = await request(app).post("/register").send({ 23 | first_name: "John", 24 | last_name: "Doe", 25 | email: "john@gmail.com", 26 | password: "123456", 27 | }); 28 | expect(response.statusCode).toEqual(201); 29 | expect(response.body.message).toEqual("Registered successfully!"); 30 | }); 31 | 32 | it("should throw validation error if required input are missing when registering", async () => { 33 | const response = await request(app).post("/register").send({ 34 | first_name: "", 35 | last_name: "", 36 | email: "", 37 | password: "", 38 | }); 39 | 40 | expect(response.statusCode).toEqual(400); 41 | expect(response.body).toHaveProperty("errors"); 42 | }); 43 | 44 | it("should login a user successfully", async () => { 45 | const response = await request(app).post("/login").send({ 46 | email: "carter@gmail.com", 47 | password: "123456", 48 | }); 49 | expect(response.statusCode).toEqual(200); 50 | expect(response.body.message).toEqual("Logged in successfully!"); 51 | expect(response.body).toHaveProperty("token"); 52 | }); 53 | 54 | it("should throw validation error if required input are missing when login", async () => { 55 | const response = await request(app).post("/login").send({ 56 | email: "", 57 | password: "", 58 | }); 59 | expect(response.statusCode).toEqual(400); 60 | expect(response.body).toHaveProperty("errors"); 61 | }); 62 | 63 | it("should throw error if login details are not valid", async () => { 64 | const response = await request(app).post("/login").send({ 65 | email: "invalid@gmail.com", 66 | password: "12345", 67 | }); 68 | expect(response.statusCode).toEqual(401); 69 | expect(response.body.message).toEqual("Invalid email or password"); 70 | }); 71 | 72 | it("should get user profile successfully", async () => { 73 | const response = await request(app) 74 | .get("/auth/profile") 75 | .set({ Authorization: `Bearer ${token}` }) 76 | .send(); 77 | expect(response.statusCode).toEqual(200); 78 | expect(response.body.message).toEqual("Returned profile successfully"); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /tests/wallet.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../app"); 3 | const axios = require("axios"); 4 | const MockAdapter = require("axios-mock-adapter"); 5 | 6 | let token, secondToken, mock; 7 | const setWalletPin = async (token) => { 8 | return request(app) 9 | .post("/wallet/set-pin") 10 | .set({ Authorization: `Bearer ${token}` }) 11 | .send({ 12 | pin: "1111", 13 | confirm_pin: "1111", 14 | }); 15 | }; 16 | 17 | beforeAll(async () => { 18 | await request(app).post("/register").send({ 19 | first_name: "Usman", 20 | last_name: "Salami", 21 | email: "usman@gmail.com", 22 | password: "123456", 23 | }); 24 | 25 | await request(app).post("/register").send({ 26 | first_name: "Mary", 27 | last_name: "Doe", 28 | email: "mary@gmail.com", 29 | password: "123456", 30 | }); 31 | 32 | const response = await request(app).post("/login").send({ 33 | email: "usman@gmail.com", 34 | password: "123456", 35 | }); 36 | token = response.body.token; 37 | 38 | await setWalletPin(token); 39 | 40 | const secondResponse = await request(app).post("/login").send({ 41 | email: "mary@gmail.com", 42 | password: "123456", 43 | }); 44 | 45 | secondToken = secondResponse.body.token; 46 | 47 | mock = new MockAdapter(axios); 48 | }); 49 | 50 | describe("Wallet", () => { 51 | it("should set wallet pin successfully", async () => { 52 | const response = await setWalletPin(token); 53 | expect(response.statusCode).toEqual(201); 54 | expect(response.body.message).toEqual("Set pin successfully!"); 55 | }); 56 | 57 | it("should throw validation error if required input are missing when setting pin", async () => { 58 | const response = await request(app) 59 | .post("/wallet/set-pin") 60 | .set({ Authorization: `Bearer ${token}` }) 61 | .send({ 62 | pin: "", 63 | confirm_pin: "", 64 | }); 65 | expect(response.statusCode).toEqual(400); 66 | expect(response.body).toHaveProperty("errors"); 67 | }); 68 | 69 | it("should initialize wallet funding successfully", async () => { 70 | mock.onPost("https://api.flutterwave.com/v3/payments").reply(200, { 71 | data: { 72 | link: "https://ravemodal-dev.herokuapp.com/v3/hosted/pay/f3859ae0ee1b5e4d0413", 73 | }, 74 | }); 75 | 76 | const response = await request(app) 77 | .post("/wallet/fund") 78 | .set({ Authorization: `Bearer ${token}` }) 79 | .send({ 80 | amount: "500", 81 | }); 82 | 83 | expect(response.statusCode).toEqual(201); 84 | expect(response.body.message).toEqual("Initialized Wallet Funding"); 85 | expect(response.body).toHaveProperty("paymentLink"); 86 | }); 87 | 88 | it("should throw if wallet pin is not set before funding wallet", async () => { 89 | const response = await request(app) 90 | .post("/wallet/fund") 91 | .set({ Authorization: `Bearer ${secondToken}` }) 92 | .send({ 93 | amount: "500", 94 | }); 95 | expect(response.statusCode).toEqual(400); 96 | expect(response.body.message).toEqual("Please set your wallet pin before performing any transaction"); 97 | }); 98 | 99 | it("should throw validation error if required input are missing when funding wallet", async () => { 100 | const response = await request(app) 101 | .post("/wallet/fund") 102 | .set({ Authorization: `Bearer ${token}` }) 103 | .send({ 104 | amount: "", 105 | }); 106 | 107 | expect(response.statusCode).toEqual(400); 108 | expect(response.body).toHaveProperty("errors"); 109 | }); 110 | 111 | it("should verify wallet funding successfully", async () => { 112 | mock.onGet("https://api.flutterwave.com/v3/transactions/288200108/verify").reply(200, { 113 | status: "success", 114 | message: "Transaction fetched successfully", 115 | data: { 116 | id: 288200108, 117 | tx_ref: "PID-12C3TH95ZY", 118 | flw_ref: "HomerSimpson/FLW275407301", 119 | device_fingerprint: "N/A", 120 | amount: 500, 121 | currency: "NGN", 122 | charged_amount: 500, 123 | app_fee: 1.4, 124 | merchant_fee: 0, 125 | processor_response: "Approved by Financial Institution", 126 | auth_model: "PIN", 127 | ip: "::ffff:10.5.179.3", 128 | narration: "CARD Transaction ", 129 | status: "successful", 130 | payment_type: "card", 131 | created_at: "2021-07-15T14:06:55.000Z", 132 | account_id: 17321, 133 | card: { 134 | first_6digits: "455605", 135 | last_4digits: "2643", 136 | issuer: "MASTERCARD GUARANTY TRUST BANK Mastercard Naira Debit Card", 137 | country: "NG", 138 | type: "MASTERCARD", 139 | token: "flw-t1nf-93da56b24f8ee332304cd2eea40a1fc4-m03k", 140 | expiry: "01/23", 141 | }, 142 | meta: null, 143 | amount_settled: 7500, 144 | customer: { 145 | id: 370672, 146 | phone_number: null, 147 | name: "Usman Salami", 148 | email: "usman@gmail.com", 149 | created_at: "2020-04-30T20:09:56.000Z", 150 | }, 151 | }, 152 | }); 153 | const response = await request(app) 154 | .get("/wallet/verify?status=successful&tx_ref=PID-12C3TH95ZY&transaction_id=288200108") 155 | .set({ Authorization: `Bearer ${token}` }) 156 | .send(); 157 | 158 | expect(response.statusCode).toEqual(201); 159 | expect(response.body.message).toEqual("Wallet Funded Successfully"); 160 | }); 161 | 162 | it("should transfer funding successfully", async () => { 163 | const response = await request(app) 164 | .post("/wallet/transfer") 165 | .set({ Authorization: `Bearer ${token}` }) 166 | .send({ 167 | amount: 100, 168 | wallet_code_or_email: "mary@gmail.com", 169 | wallet_pin: "1111", 170 | }); 171 | 172 | expect(response.statusCode).toEqual(201); 173 | expect(response.body.message).toEqual("Fund Transfer Successful"); 174 | }); 175 | 176 | it("should withdraw fund successfully", async () => { 177 | const response = await request(app) 178 | .post("/wallet/withdraw") 179 | .set({ Authorization: `Bearer ${token}` }) 180 | .send({ 181 | amount: 400, 182 | bank_code: "044", 183 | account_number: "0690000040", 184 | wallet_pin: "1111", 185 | }); 186 | 187 | expect(response.statusCode).toEqual(201); 188 | expect(response.body.message).toEqual("Withdrawal Successful"); 189 | }); 190 | 191 | it("should throw validation error if required input are missing when withdrawing fund", async () => { 192 | const response = await request(app) 193 | .post("/wallet/withdraw") 194 | .set({ Authorization: `Bearer ${token}` }) 195 | .send({ 196 | amount: "", 197 | bank_code: "", 198 | account_number: "", 199 | }); 200 | 201 | expect(response.statusCode).toEqual(400); 202 | expect(response.body).toHaveProperty("errors"); 203 | }); 204 | 205 | it("should get wallet balance successfully", async () => { 206 | const response = await request(app) 207 | .get("/wallet/balance") 208 | .set({ Authorization: `Bearer ${token}` }) 209 | .send(); 210 | expect(response.statusCode).toEqual(200); 211 | expect(response.body.message).toEqual("Returned wallet balance successfully"); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /utils/catchasync.js: -------------------------------------------------------------------------------- 1 | const catchAsync = (fn) => (req, res, next) => { 2 | Promise.resolve(fn(req, res, next)).catch((err) => next(err)); 3 | }; 4 | 5 | module.exports = catchAsync; 6 | -------------------------------------------------------------------------------- /utils/errors/app.error.js: -------------------------------------------------------------------------------- 1 | class AppError extends Error { 2 | constructor(message, statusCode) { 3 | super(message); 4 | this.statusCode = statusCode; 5 | this.isOperational = true; 6 | Error.captureStackTrace(this, this.constructor); 7 | } 8 | } 9 | 10 | module.exports = AppError; -------------------------------------------------------------------------------- /utils/errors/badrequest.error.js: -------------------------------------------------------------------------------- 1 | const httpStatus = require("http-status"); 2 | const AppError = require("./app.error"); 3 | 4 | class BadRequestError extends AppError { 5 | constructor(message = httpStatus[httpStatus.BAD_REQUEST]) { 6 | super(message); 7 | this.statusCode = httpStatus.BAD_REQUEST; 8 | this.isOperational = true; 9 | } 10 | } 11 | 12 | module.exports = BadRequestError; 13 | -------------------------------------------------------------------------------- /utils/errors/notfound.error.js: -------------------------------------------------------------------------------- 1 | const httpStatus = require("http-status"); 2 | const AppError = require("./app.error"); 3 | 4 | class NotFoundError extends AppError { 5 | constructor(message = httpStatus[httpStatus.NOT_FOUND]) { 6 | super(message); 7 | this.statusCode = httpStatus.NOT_FOUND; 8 | this.isOperational = true; 9 | } 10 | } 11 | 12 | module.exports = NotFoundError; 13 | -------------------------------------------------------------------------------- /utils/errors/unauthorized.error.js: -------------------------------------------------------------------------------- 1 | const httpStatus = require("http-status"); 2 | const AppError = require("./app.error"); 3 | 4 | class UnAuthorizedError extends AppError { 5 | constructor(message = httpStatus[httpStatus.UNAUTHORIZED]) { 6 | super(message); 7 | this.statusCode = httpStatus.UNAUTHORIZED; 8 | this.isOperational = true; 9 | } 10 | } 11 | 12 | module.exports = UnAuthorizedError; 13 | -------------------------------------------------------------------------------- /validations/index.js: -------------------------------------------------------------------------------- 1 | module.exports.userValidation = require('./user.validation'); 2 | module.exports.walletValidation = require('./wallet.validation'); -------------------------------------------------------------------------------- /validations/user.validation.js: -------------------------------------------------------------------------------- 1 | const { check } = require('express-validator'); 2 | const db = require('../config/db') 3 | 4 | 5 | const register = [ 6 | check('first_name', 'First name is required').not().isEmpty(), 7 | check('last_name', 'Last name is required').not().isEmpty(), 8 | check('email', 'Please include a valid email').isEmail().normalizeEmail({ gmail_remove_dots: true }).custom(value => { 9 | return db.select('*').from('users').where('email', value).first().then(user => { 10 | if (user) { 11 | return Promise.reject('E-mail already in use'); 12 | } 13 | })}), 14 | check('password', 'Password must be 6 or more characters').isLength({ min: 6 }) 15 | ] 16 | 17 | const login = [ 18 | check('email', 'Please include a valid email').isEmail(), 19 | check('password', 'Password is required').not().isEmpty(), 20 | ] 21 | 22 | module.exports = { 23 | register, 24 | login 25 | }; -------------------------------------------------------------------------------- /validations/wallet.validation.js: -------------------------------------------------------------------------------- 1 | const { check } = require("express-validator"); 2 | 3 | const setWalletPin = [ 4 | check("pin", "Pin is required") 5 | .not() 6 | .isEmpty() 7 | .isLength({ min: 4, max: 4 }) 8 | .withMessage("Pin must contain only 4 numbers") 9 | .isInt() 10 | .withMessage("Pin must contain only numbers"), 11 | check("confirm_pin", "Confirm pin is required") 12 | .not() 13 | .isEmpty() 14 | .isLength({ min: 4, max: 4 }) 15 | .withMessage("Confirm pin must contain only 4 numbers") 16 | .isInt() 17 | .withMessage("Confirm pin must contain only numbers") 18 | .custom((value, { req }) => { 19 | if (value !== req.body.pin) { 20 | return Promise.reject("confirm pin must be the same as pin"); 21 | } else { 22 | return true; 23 | } 24 | }), 25 | ]; 26 | 27 | const fundWallet = [ 28 | check("amount", "Amount is required") 29 | .not() 30 | .isEmpty() 31 | .isCurrency() 32 | .withMessage("amount must be a currency"), 33 | check("frontend_base_url") 34 | .isURL() 35 | .optional() 36 | ]; 37 | 38 | const transferFund = [ 39 | check("amount", "Amount is required") 40 | .not() 41 | .isEmpty() 42 | .isCurrency() 43 | .withMessage("amount must be a currency"), 44 | check("wallet_code_or_email", "Please provide either recipient wallet code or email") 45 | .not() 46 | .isEmpty(), 47 | check("wallet_pin", "Wallet pin is required") 48 | .not() 49 | .isEmpty() 50 | ]; 51 | 52 | const withdrawFund = [ 53 | check("amount", "Amount is required") 54 | .not() 55 | .isEmpty() 56 | .isCurrency() 57 | .withMessage("amount must be a currency"), 58 | check("bank_code", "Bank code is required") 59 | .not() 60 | .isEmpty() 61 | .isLength({ min: 3, max: 3 }) 62 | .withMessage("Bank code contain only 3 numbers"), 63 | check("account_number", "Bank code is required") 64 | .not() 65 | .isEmpty() 66 | .isLength({ min: 10, max: 10 }) 67 | .withMessage("Account number contain only 10 numbers"), 68 | check("wallet_pin", "Wallet pin is required") 69 | .not() 70 | .isEmpty() 71 | ]; 72 | 73 | module.exports = { 74 | setWalletPin, 75 | fundWallet, 76 | transferFund, 77 | withdrawFund 78 | }; 79 | --------------------------------------------------------------------------------