├── config └── config.js ├── routes ├── route_index.js └── nuban_util.js ├── package.json ├── index.js ├── .gitignore └── README.md /config/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "NUBANValidatorAPI", 3 | env: process.env.NODE_ENV || "development", 4 | port: process.env.PORT || 3000, 5 | base_url: process.env.BASE_URL || "http://localhost:3000" 6 | }; 7 | -------------------------------------------------------------------------------- /routes/route_index.js: -------------------------------------------------------------------------------- 1 | var nubanUtil = require("./nuban_util"); 2 | 3 | module.exports = function(server) { 4 | server.get("/", (req, res, next) => { 5 | res.send("Initial page here"); 6 | }); 7 | 8 | server.get("/accounts/:account(^\\d{10}$)/banks", (req, res, next) => { 9 | nubanUtil.getAccountBanks(req, res, next); 10 | }); 11 | 12 | server.post("/banks/:bank(^\\d{3}$)/accounts", (req, res, next) => { 13 | nubanUtil.createAccountWithSerial(req, res, next); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nubanvalidator", 3 | "version": "1.0.0", 4 | "description": "Provides different utilities to validate a Nigerian bank account number", 5 | "main": "index.js", 6 | "dependencies": { 7 | "restify": "^8.3.2", 8 | "restify-errors": "^8.0.0", 9 | "restify-plugins": "^1.6.0" 10 | }, 11 | "devDependencies": {}, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "NUBAN" 17 | ], 18 | "author": "Hafiz Adewuyi", 19 | "license": "ISC" 20 | } 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var restify = require("restify"); 2 | var config = require("./config/config"); 3 | const restifyPlugins = require("restify-plugins"); 4 | var route_link = require("./routes/route_index"); 5 | var restify_err = require("restify-errors"); 6 | 7 | const server = restify.createServer({ 8 | name: config.name, 9 | version: config.version 10 | }); 11 | 12 | server.use( 13 | restifyPlugins.jsonBodyParser({ 14 | mapParams: true 15 | }) 16 | ); 17 | 18 | server.use(restifyPlugins.acceptParser(server.acceptable)); 19 | 20 | server.use( 21 | restifyPlugins.queryParser({ 22 | mapParams: true 23 | }) 24 | ); 25 | 26 | server.use(restifyPlugins.fullResponse()); 27 | 28 | server.listen(config.port, function() { 29 | console.log("%s listening at %s", server.name, config.base_url); 30 | }); 31 | 32 | route_link(server); 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | *.pid.lock 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | # nyc test coverage 17 | .nyc_output 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | # Bower dependency directory (https://bower.io/) 21 | bower_components 22 | # node-waf configuration 23 | .lock-wscript 24 | # Compiled binary addons (https://nodejs.org/api/addons.html) 25 | build/Release 26 | # Dependency directories 27 | node_modules/ 28 | jspm_packages/ 29 | # TypeScript v1 declaration files 30 | typings/ 31 | # Optional npm cache directory 32 | .npm 33 | # Optional eslint cache 34 | .eslintcache 35 | # Optional REPL history 36 | .node_repl_history 37 | # Output of 'npm pack' 38 | *.tgz 39 | # Yarn Integrity file 40 | .yarn-integrity 41 | # dotenv environment variables file 42 | .env 43 | # parcel-bundler cache (https://parceljs.org/) 44 | .cache 45 | # next.js build output 46 | .next 47 | # nuxt.js build output 48 | .nuxt 49 | # vuepress build output 50 | .vuepress/dist 51 | # Serverless directories 52 | .serverless -------------------------------------------------------------------------------- /routes/nuban_util.js: -------------------------------------------------------------------------------- 1 | var { NotFoundError } = require("restify-errors"); 2 | 3 | const banks = [ 4 | { name: "ACCESS BANK", code: "044" }, 5 | { name: "CITIBANK", code: "023" }, 6 | { name: "DIAMOND BANK", code: "063" }, 7 | { name: "ECOBANK NIGERIA", code: "050" }, 8 | { name: "FIDELITY BANK", code: "070" }, 9 | { name: "FIRST BANK OF NIGERIA", code: "011" }, 10 | { name: "FIRST CITY MONUMENT BANK", code: "214" }, 11 | { name: "GUARANTY TRUST BANK", code: "058" }, 12 | { name: "HERITAGE BANK", code: "030" }, 13 | { name: "JAIZ BANK", code: "301" }, 14 | { name: "KEYSTONE BANK", code: "082" }, 15 | { name: "PROVIDUS BANK", code: "101" }, 16 | { name: "SKYE BANK", code: "076" }, 17 | { name: "STANBIC IBTC BANK", code: "221" }, 18 | { name: "STANDARD CHARTERED BANK", code: "068" }, 19 | { name: "STERLING BANK", code: "232" }, 20 | { name: "SUNTRUST", code: "100" }, 21 | { name: "UNION BANK OF NIGERIA", code: "032" }, 22 | { name: "UNITED BANK FOR AFRICA", code: "033" }, 23 | { name: "UNITY BANK", code: "215" }, 24 | { name: "WEMA BANK", code: "035" }, 25 | { name: "ZENITH BANK", code: "057" } 26 | ]; 27 | 28 | const seed = "373373373373"; 29 | const nubanLength = 10; 30 | const serialNumLength = 9; 31 | let error; 32 | 33 | module.exports = { 34 | getAccountBanks: (req, res, next) => { 35 | let accountNumber = req.params.account; 36 | 37 | let accountBanks = []; 38 | 39 | banks.forEach((item, index) => { 40 | if (isBankAccountValid(accountNumber, item.code)) { 41 | accountBanks.push(item); 42 | } 43 | }); 44 | 45 | res.send(accountBanks); 46 | }, 47 | createAccountWithSerial: (req, res, next) => { 48 | let bankCode = req.params.bank; 49 | let bank = banks.find(bank => bank.code == bankCode); 50 | 51 | if (!bank) { 52 | return next( 53 | new NotFoundError( 54 | "We do not recognize this code as a Nigerian commercial bank code" 55 | ) 56 | ); 57 | } 58 | 59 | try { 60 | let serialNumber = req.body.serialNumber.padStart(serialNumLength, "0"); 61 | let nuban = `${serialNumber}${generateCheckDigit( 62 | serialNumber, 63 | bankCode 64 | )}`; 65 | 66 | let account = { 67 | serialNumber, 68 | nuban, 69 | bankCode, 70 | bank 71 | }; 72 | 73 | res.send(account); 74 | } catch (err) { 75 | next(err); 76 | } 77 | } 78 | }; 79 | 80 | const generateCheckDigit = (serialNumber, bankCode) => { 81 | if (serialNumber.length > serialNumLength) { 82 | throw new Error( 83 | `Serial number should be at most ${serialNumLength}-digits long.` 84 | ); 85 | } 86 | 87 | serialNumber = serialNumber.padStart(serialNumLength, "0"); 88 | let cipher = bankCode + serialNumber; 89 | let sum = 0; 90 | 91 | // Step 1. Calculate A*3+B*7+C*3+D*3+E*7+F*3+G*3+H*7+I*3+J*3+K*7+L*3 92 | cipher.split("").forEach((item, index) => { 93 | sum += item * seed[index]; 94 | }); 95 | 96 | // Step 2: Calculate Modulo 10 of your result i.e. the remainder after dividing by 10 97 | sum %= 10; 98 | 99 | // Step 3. Subtract your result from 10 to get the Check Digit 100 | let checkDigit = 10 - sum; 101 | 102 | // Step 4. If your result is 10, then use 0 as your check digit 103 | checkDigit = checkDigit == 10 ? 0 : checkDigit; 104 | 105 | return checkDigit; 106 | }; 107 | 108 | /** 109 | * Algorithm source: https://www.cbn.gov.ng/OUT/2011/CIRCULARS/BSPD/NUBAN%20PROPOSALS%20V%200%204-%2003%2009%202010.PDF 110 | * The approved NUBAN format ABC-DEFGHIJKL-M where 111 | * ABC is the 3-digit bank code assigned by the CBN 112 | * DEFGHIJKL is the NUBAN Account serial number 113 | * M is the NUBAN Check Digit, required for account number validation 114 | * @param {*} accountNumber 115 | * @param {*} bankCode 116 | */ 117 | const isBankAccountValid = (accountNumber, bankCode) => { 118 | if (!accountNumber || !accountNumber.length == nubanLength) { 119 | error = "NUBAN must be %s digits long" % nubanLength; 120 | return false; 121 | } 122 | 123 | let serialNumber = accountNumber.substring(0, 9); 124 | let checkDigit = generateCheckDigit(serialNumber, bankCode); 125 | 126 | return checkDigit == accountNumber[9]; 127 | }; 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NUBAN (Nigerian Uniform Bank Account Number) Algorithm 2 | 3 | This repo contains the algorithm for generating and validating a NUBAN (Nigeria Uniform Bank Account Number) in Javascript. The algorithm is based on [this here CBN specification](https://www.cbn.gov.ng/OUT/2011/CIRCULARS/BSPD/NUBAN%20PROPOSALS%20V%200%204-%2003%2009%202010.PDF) for the 10-digit NUBAN. 10-digit is stated because CBN announced not too long ago that it's considering updating the specification for a NUBAN; which might see the NUBAN getting up to 16-digits in length. 4 | 5 | ## Setting up 6 | 7 | - Clone the repo. 8 | - Navigate to the project root folder via terminal. 9 | - Assuming node is installed on your computer: 10 | - Restore the node packages which the application depends on using `npm install`. 11 | - Run `node index.js`. 12 | 13 | ## API Endpoints 14 | 15 | ### 1. **Get Account Banks** 16 | 17 | Given any 10-digit Nigerian bank account number, this endpoint returns a JSON array of banks where that account number could be valid. 18 | 19 | A common application of this algorithm in Nigeria today is to cut down the list of banks on USSD interfaces from about 23 to less than 5 after the user enters their bank account number (NUBAN). This comes in handy because a USSD screen can display at most, 160 characters at a time. 20 | 21 | _Specification_ 22 | 23 | `GET /accounts/{10-digit-NUBAN}/banks` 24 | 25 | _Sample request_ 26 | 27 | `GET /accounts/5050114930/banks` 28 | 29 | _Sample response_ 30 | 31 | ```json 32 | [ 33 | { 34 | "name": "PROVIDUS BANK", 35 | "code": "101" 36 | }, 37 | { 38 | "name": "STANDARD CHARTERED BANK", 39 | "code": "068" 40 | }, 41 | { 42 | "name": "WEMA BANK", 43 | "code": "035" 44 | }, 45 | { 46 | "name": "ZENITH BANK", 47 | "code": "057" 48 | } 49 | ] 50 | ``` 51 | 52 | _Bank model_ 53 | 54 | - `name`: The name of the Nigerian bank. 55 | - `code`: The CBN unique identifier for the Nigerian Bank. This is a 3-digit literal. 56 | 57 | ### 2. **Generate Bank Account** 58 | 59 | Given any 9-digit number (account serial number) and a 3-digit Nigerian bank code, this endpoint returns the full account number. Here is [a list of Nigerian bank codes](https://github.com/tomiiide/nigerian-banks/blob/master/banks.json) you can use to test this. 60 | 61 | _Specification_ 62 | 63 | `POST /banks/{3-digit bank code}/accounts` 64 | 65 | ```json 66 | { 67 | "serialNumber" 68 | } 69 | ``` 70 | 71 | \*\* `serialNumber` should be 9-digits or less. If less than 9-digits, it will be left zero-padded. 72 | 73 | _Sample request_ 74 | 75 | Generate a GTBank account number with serial number: '1656322' 76 | 77 | `POST /accounts/058/banks` (058 is bank code for GTBank) 78 | 79 | ```json 80 | { 81 | "serialNumber": "1656322" 82 | } 83 | ``` 84 | 85 | _Sample response_ 86 | 87 | ```json 88 | { 89 | "serialNumber": "001656322", 90 | "nuban": "0016563228", 91 | "name": "Hafiz Adewuyi's GTBank account number. Donations are invited!", 92 | "bankCode": "058", 93 | "bank": { 94 | "name": "GUARANTY TRUST BANK", 95 | "code": "058" 96 | } 97 | } 98 | ``` 99 | 100 | ## To-do 101 | 102 | - List of banks to be implemented such that it's never out-of-date. It's currently an in-memory list defined within the source code. It's better to fetch this list from a reliable and always up-to-date source of Nigerian bank information on application start. [This repo](https://github.com/tomiiide/nigerian-banks/blob/master/banks.json) seems like a good start. 103 | 104 | - Build an SPA which offers the following features: 105 | 106 | - Generate a valid account number for any Nigerian bank 107 | - Give me the first 9-digits of your NUBAN and I'll tell you your name 👻👻👻 108 | - Upload a list of NUBAN + bank codes for validation 109 | 110 | | Account number | Bank Code | Valid | 111 | | -------------- | --------- | ----- | 112 | | 0010020030 | 001 | Yes | 113 | | 0010020030 | 005 | Yes | 114 | | 0010020030 | 050 | No | 115 | | 0010020030 | 061 | Yes | 116 | 117 | - Enhance the 'Generate Bank Account' endpoint to generate a serial number itself and return the corresponding NUBAN when the request body is empty or does not contain a valid `serialNumber`. 118 | 119 | - Write unit tests. I think a good unit test would be to run at least, 10,000 real bank accounts (`{ accountNumber, bankCode }`) from various banks in Nigeria through the code and verify that for each account, the list of banks returned contains the actual bank. 120 | 121 | - Deploy the application to a free Heroku container. 122 | --------------------------------------------------------------------------------