├── .gitignore ├── jest.config.js ├── .github └── workflows │ └── main.yml ├── src ├── helpers.ts ├── nuban_util.ts └── banks.ts ├── package.json ├── tsconfig.json ├── test └── predictBanks.test.ts └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { createDefaultPreset } = require('ts-jest') 2 | 3 | module.exports = { 4 | transform: { 5 | ...createDefaultPreset().transform, 6 | }, 7 | moduleFileExtensions: [ 8 | 'js', 9 | 'ts', 10 | ], 11 | testMatch: [ 12 | '**/*.test.(ts|js)', 13 | ], 14 | testEnvironment: 'node', 15 | preset: 'ts-jest', 16 | }; -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Check out code 11 | uses: actions/checkout@v2 12 | 13 | - name: Use Node.js 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: '14' 17 | 18 | - name: Install dependencies 19 | run: npm install 20 | 21 | - name: Run tests 22 | run: npm test 23 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | export const bankCodeWeights: number[] = [3, 7, 3, 3, 7, 3]; 2 | 3 | export const serialNumberWeights: number[] = [3, 7, 3, 3, 7, 3, 3, 7, 3]; 4 | 5 | export const calculateWeightedSum = (value: string, weights: number[]): number => { 6 | if (value.length !== weights.length) { 7 | throw new Error('value and weights must have the same length'); 8 | } 9 | 10 | return value.split('').reduce((sum, digit, index) => sum + Number(digit) * weights[index], 0); 11 | 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "predict-nuban-banks", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "ts-node src/getBanks.ts", 9 | "test": "jest" 10 | }, 11 | "keywords": [ 12 | "nuban", 13 | "cbn", 14 | "bank", 15 | "predict" 16 | ], 17 | "author": "Wahab Balogun", 18 | "license": "ISC", 19 | "devDependencies": { 20 | "@types/jest": "^29.5.12", 21 | "globals": "^15.8.0", 22 | "jest": "^29.7.0", 23 | "ts-jest": "^29.2.2" 24 | }, 25 | "dependencies": { 26 | "ts-node": "^10.9.2", 27 | "typescript": "^5.5.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | "sourceMap": true, /* Generates corresponding '.map' file. */ 6 | "outDir": "dist", /* Redirect output structure to the directory. */ 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "esModuleInterop": true, 12 | "types": ["jest"] 13 | }, 14 | "include": ["src/**/*"] 15 | } -------------------------------------------------------------------------------- /src/nuban_util.ts: -------------------------------------------------------------------------------- 1 | import {bankCodeWeights, calculateWeightedSum, serialNumberWeights} from "./helpers"; 2 | import {BANKS} from "./banks"; 3 | 4 | export type Bank = { id?: string | null, name: string; code: string } 5 | 6 | export const computeCheckDigit = (bankCode: string, serialNumber: string) => { 7 | const result = calculateWeightedSum(bankCode, bankCodeWeights) + calculateWeightedSum(serialNumber, serialNumberWeights) 8 | 9 | const subtractionResult = 10 - (result % 10) 10 | 11 | return subtractionResult === 10 ? 0 : subtractionResult 12 | } 13 | 14 | /** 15 | * https://www.cbn.gov.ng/out/2018/psmd/exposure%20circular%20for%20nuban.pdf 16 | * @param accountNumber 17 | * @param bankCode 18 | */ 19 | export const isBankAccountValid = (accountNumber: string, bankCode: string) => { 20 | if (accountNumber.length !== 10) { 21 | throw new Error('Invalid account number, account number must be 10 digits long') 22 | } 23 | 24 | let paddedBankCode = bankCode.replace(/\D/g, '') 25 | 26 | if (paddedBankCode.length === 3) { 27 | paddedBankCode = `000${paddedBankCode}` 28 | } else if (paddedBankCode.length === 5) { 29 | paddedBankCode = `9${paddedBankCode}` 30 | } 31 | 32 | if (paddedBankCode.length !== 6) { 33 | throw new Error(`Invalid bank code, bank code must be 3, 5 or 6 digits long. ${paddedBankCode} is ${paddedBankCode.length} digits long`) 34 | } 35 | 36 | const serialNumber = accountNumber.substring(0, 9) 37 | 38 | const accountCheckDigit = accountNumber[9] 39 | 40 | const checkDigit = computeCheckDigit(paddedBankCode, serialNumber) 41 | 42 | return checkDigit?.toString() === accountCheckDigit 43 | } 44 | 45 | export const getPossibleBanks = (accountNumber: string, banks: T[] = BANKS as T[]): T[] => { 46 | return banks.filter((bank) => isBankAccountValid(accountNumber, bank.code)) 47 | } -------------------------------------------------------------------------------- /src/banks.ts: -------------------------------------------------------------------------------- 1 | 2 | // Comprehensive list of all banks https://gist.github.com/03balogun/c6386aaea439f18ffabd9892112ef767 3 | export const BANKS = [ 4 | { 5 | "name": "Access Bank", 6 | "code": "044" 7 | }, 8 | { 9 | "name": "Access Bank (Diamond)", 10 | "code": "063" 11 | }, 12 | { 13 | "name": "Carbon", 14 | "code": "565" 15 | }, 16 | { 17 | "name": "Ecobank Nigeria", 18 | "code": "050" 19 | }, 20 | { 21 | "name": "Fidelity Bank", 22 | "code": "070" 23 | }, 24 | { 25 | "name": "First Bank of Nigeria", 26 | "code": "011" 27 | }, 28 | { 29 | "name": "First City Monument Bank", 30 | "code": "214" 31 | }, 32 | { 33 | "name": "Guaranty Trust Bank", 34 | "code": "058" 35 | }, 36 | { 37 | "name": "Jaiz Bank", 38 | "code": "301" 39 | }, 40 | { 41 | "name": "Keystone Bank", 42 | "code": "082" 43 | }, 44 | { 45 | "name": "Polaris Bank", 46 | "code": "076" 47 | }, 48 | { 49 | "name": "Providus Bank", 50 | "code": "101" 51 | }, 52 | { 53 | "name": "Rubies MFB", 54 | "code": "125" 55 | }, 56 | { 57 | "name": "Signature Bank Ltd", 58 | "code": "106" 59 | }, 60 | { 61 | "name": "Stanbic IBTC Bank", 62 | "code": "221" 63 | }, 64 | { 65 | "name": "Standard Chartered Bank", 66 | "code": "068" 67 | }, 68 | { 69 | "name": "Sterling Bank", 70 | "code": "232" 71 | }, 72 | { 73 | "name": "Titan Bank", 74 | "code": "102" 75 | }, 76 | { 77 | "name": "Union Bank of Nigeria", 78 | "code": "032" 79 | }, 80 | { 81 | "name": "United Bank For Africa", 82 | "code": "033" 83 | }, 84 | { 85 | "name": "Unity Bank", 86 | "code": "215" 87 | }, 88 | { 89 | "name": "VFD Microfinance Bank Limited", 90 | "code": "566" 91 | }, 92 | { 93 | "name": "Wema Bank", 94 | "code": "035" 95 | }, 96 | { 97 | "name": "Zenith Bank", 98 | "code": "057" 99 | }, 100 | { 101 | "name": "OPay Digital Services Limited (OPay)", 102 | "code": "999992" 103 | }, 104 | { 105 | "name": "Paga", 106 | "code": "100002" 107 | }, 108 | { 109 | "name": "PalmPay", 110 | "code": "999991" 111 | }, 112 | { 113 | "name": "Paystack-Titan", 114 | "code": "100039" 115 | }, 116 | { 117 | "name": "Sparkle Microfinance Bank", 118 | "code": "51310" 119 | }, 120 | { 121 | "name": "Moniepoint MFB", 122 | "code": "50515" 123 | }, 124 | { 125 | "name": "GoMoney", 126 | "code": "100022" 127 | }, 128 | ] 129 | -------------------------------------------------------------------------------- /test/predictBanks.test.ts: -------------------------------------------------------------------------------- 1 | import {getPossibleBanks, isBankAccountValid} from "../src/nuban_util"; 2 | 3 | describe('isBankAccountValid', () => { 4 | it('should throw an error if account number is not 10 digits long', () => { 5 | expect(() => isBankAccountValid('123456789', '123')).toThrow('Invalid account number, account number must be 10 digits long'); 6 | }); 7 | 8 | it('should throw an error if bank code is not 3, 5 or 6 digits long', () => { 9 | expect(() => isBankAccountValid('1234567890', '12')).toThrow('Invalid bank code, bank code must be 3, 5 or 6 digits long'); 10 | }); 11 | 12 | it('should return true if account number and bank code are valid and match', () => { 13 | expect(isBankAccountValid('0010246780', '044')).toBe(true); 14 | }); 15 | 16 | it('should return false if account number and bank code are valid but do not match', () => { 17 | expect(isBankAccountValid('1234567890', '123')).toBe(false); 18 | }); 19 | 20 | }); 21 | 22 | describe('getPossibleBanks', () => { 23 | it('should return an array of banks for a valid account number', () => { 24 | const result = getPossibleBanks('0010246780'); 25 | expect(result).toBeInstanceOf(Array); 26 | expect(result[0]).toHaveProperty('name'); 27 | expect(result[0]).toHaveProperty('code'); 28 | }); 29 | 30 | it('should return an empty array for an invalid account number', () => { 31 | const result = getPossibleBanks('00000000xx'); 32 | expect(result).toBeInstanceOf(Array); 33 | expect(result).toHaveLength(0); 34 | }); 35 | 36 | it('should include bank code 50515 (Moniepoint)', () => { 37 | const result = getPossibleBanks('5522116946'); 38 | expect(result).toBeInstanceOf(Array); 39 | expect(result.some((bank) => bank.code === '50515')).toBe(true); 40 | }); 41 | 42 | it('should include bank code 999991 (PalmPay)', () => { 43 | const result = getPossibleBanks('8106136519'); 44 | expect(result).toBeInstanceOf(Array); 45 | expect(result.some((bank) => bank.code === '999991')).toBe(true); 46 | }); 47 | 48 | it('should include bank code 044 (Access Bank)', () => { 49 | const result = getPossibleBanks('0010246780'); 50 | expect(result).toBeInstanceOf(Array); 51 | expect(result.some((bank) => bank.code === '044')).toBe(true); 52 | }); 53 | 54 | it('should include bank code 221 (Stanbic IBTC Bank)', () => { 55 | const result = getPossibleBanks('0054556411'); 56 | expect(result).toBeInstanceOf(Array); 57 | expect(result.some((bank) => bank.code === '221')).toBe(true); 58 | }); 59 | 60 | it('should include bank code 057 (Zenith)', () => { 61 | const result = getPossibleBanks('1012854016'); 62 | expect(result).toBeInstanceOf(Array); 63 | expect(result.some((bank) => bank.code === '057')).toBe(true); 64 | }); 65 | 66 | it('should include bank code 058 (GTBank)', () => { 67 | const result = getPossibleBanks('0108024071'); 68 | expect(result).toBeInstanceOf(Array); 69 | expect(result.some((bank) => bank.code === '058')).toBe(true); 70 | }); 71 | 72 | it('should include bank code 033 (UBA)', () => { 73 | const result = getPossibleBanks('1018044721'); 74 | expect(result).toBeInstanceOf(Array); 75 | expect(result.some((bank) => bank.code === '033')).toBe(true); 76 | }); 77 | 78 | it('should include bank code 011 (First Banks)', () => { 79 | const result = getPossibleBanks('2022323697'); 80 | expect(result).toBeInstanceOf(Array); 81 | expect(result.some((bank) => bank.code === '011')).toBe(true); 82 | }); 83 | 84 | it('should include bank code 070 (Fidelity Bank)', () => { 85 | const result = getPossibleBanks('5600026567'); 86 | expect(result).toBeInstanceOf(Array); 87 | expect(result.some((bank) => bank.code === '070')).toBe(true); 88 | }); 89 | 90 | it('should include bank code 214 (FCMB)', () => { 91 | const result = getPossibleBanks('2828744017'); 92 | expect(result).toBeInstanceOf(Array); 93 | expect(result.some((bank) => bank.code === '214')).toBe(true); 94 | }); 95 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NUBAN Bank Prediction Algorithm 2 | 3 | This utility function is designed to predict or identify the bank associated with a given Nigeria Uniform Bank Account Number (NUBAN). 4 | 5 | It validates NUBAN account numbers and provides possible banks that could own the account number. 6 | 7 | This implementation is based on the Central Bank of Nigeria's [REVISED STANDARDS ON NIGERIA UNIFORM BANK ACCOUNT NUMBER (NUBAN) SCHEME FOR DEPOSIT MONEY BANKS (DMBs) AND OTHER FINANCIAL INSTITUTIONS (OFIs) IN NIGERIA - MAR 2020](https://www.cbn.gov.ng/out/2020/psmd/revised%20standards%20on%20nigeria%20uniform%20bank%20account%20number%20(nuban)%20for%20banks%20and%20other%20financial%20institutions%20.pdf). 8 | 9 | 10 | ## Prerequisites 11 | 12 | - Node.js (v14.0.0 or later) 13 | - npm (v6.0.0 or later) 14 | - TypeScript (v5.5.3 or later) 15 | 16 | ## Installation 17 | 18 | Clone the repository and install the dependencies: 19 | 20 | ```bash 21 | git clone https://github.com/03balogun/nuban-bank-prediction-algorithm.git 22 | cd nuban-bank-prediction-algorithm 23 | npm install 24 | ``` 25 | 26 | ## Usage 27 | 28 | The utility provides two main functions: `isBankAccountValid` and `getPossibleBanks`. 29 | 30 | ### isBankAccountValid 31 | 32 | This function validates a given account number and bank code. It throws an error if the account number is not 10 digits long or if the bank code is not 3, 5 or 6 digits long. It returns `true` if the account number and bank code are valid and match, and `false` otherwise. 33 | 34 | ```typescript 35 | import { isBankAccountValid } from './src/nuban_util'; 36 | 37 | const isValid = isBankAccountValid('0010246780', '044'); 38 | console.log(isValid); // true 39 | ``` 40 | 41 | ### getPossibleBanks 42 | 43 | This function returns an array of possible banks that could own a given account number. Each bank in the array is represented as an object with name and code properties. The name is the name of the bank and the code is the bank's code as per the CBN standards. 44 | 45 | Here's an example of how you might use the function and what the response might look like: 46 | ```typescript 47 | import { getPossibleBanks } from './src/nuban_util'; 48 | 49 | const banks = getPossibleBanks('0010246780'); 50 | console.log(banks); // Array of possible banks 51 | ``` 52 | 53 | Output example: 54 | ```json 55 | [ 56 | { 57 | "name": "ACCESS BANK", 58 | "code": "044" 59 | }, 60 | { 61 | "name": "First Bank PLC", 62 | "code": "011" 63 | } 64 | ] 65 | ``` 66 | 67 | You can also pass your own custom list of banks to the getPossibleBanks function. The custom list should be an array of objects that match the Bank type. 68 | 69 | Here's an example: 70 | 71 | ```typescript 72 | import { getPossibleBanks, Bank } from './src/nuban_util'; 73 | 74 | type CustomBank = Bank & { branch: string }; 75 | 76 | const customBanks: CustomBank[] = [ 77 | { 78 | name: 'Custom Bank 1', 79 | code: '001', 80 | branch: 'Main Branch' 81 | }, 82 | { 83 | name: 'Custom Bank 2', 84 | code: '002', 85 | branch: 'Secondary Branch' 86 | } 87 | // ... more custom banks 88 | ]; 89 | 90 | const banks = getPossibleBanks('0010246780', customBanks); 91 | console.log(banks); // Array of possible banks from the custom list 92 | 93 | ``` 94 | 95 | In this example, CustomBank is a type that extends Bank and adds a branch property. The `getPossibleBanks` function is then called with the CustomBank type and the custom list of banks. 96 | 97 | ## Testing 98 | 99 | To run the tests, use the following command: 100 | 101 | ```bash 102 | npm run test 103 | ``` 104 | 105 | ## Note 106 | 107 | Please note that the list of banks provided in this utility is not exhaustive. It is a subset of all the banks available in Nigeria. Depending on your use case, you might need to update the list to include other banks or remove some banks. 108 | 109 | You can find a more comprehensive [list of banks in this gist](https://gist.github.com/03balogun/c6386aaea439f18ffabd9892112ef767). 110 | 111 | To update the bank list, modify the `BANKS` array in the `src/banks.ts` file. Each bank should be represented as an object with `name` and `code` properties. The `name` is the name of the bank and the `code` is the bank's code as per the CBN standards. 112 | 113 | ## Read More 114 | NUBAN Bank Suggestion Algorithm: Implementation and Recommendations - [https://03balogun.medium.com/nuban-bank-suggestion-algorithm-implementation-and-recommendations-ac0f13d2ce4c](https://03balogun.medium.com/nuban-bank-suggestion-algorithm-implementation-and-recommendations-ac0f13d2ce4c) 115 | 116 | ## Contributing 117 | 118 | Contributions are welcome. Please make sure to update tests as appropriate. 119 | 120 | ## License 121 | 122 | This project is licensed under the ISC License. --------------------------------------------------------------------------------