ng-bank-account-validator
Version:
Verify nigerian NUBAN bank accounts
469 lines (468 loc) • 18.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CardBrand = exports.CardType = exports.BankProperty = exports.PaymentProvider = exports.Nuban = void 0;
const constants_1 = require("./constants");
const banks_1 = require("./constants/banks");
const weighted_bank_1 = require("./constants/weighted-bank");
const types_1 = require("./types");
Object.defineProperty(exports, "BankProperty", { enumerable: true, get: function () { return types_1.BankProperty; } });
Object.defineProperty(exports, "CardBrand", { enumerable: true, get: function () { return types_1.CardBrand; } });
Object.defineProperty(exports, "CardType", { enumerable: true, get: function () { return types_1.CardType; } });
Object.defineProperty(exports, "PaymentProvider", { enumerable: true, get: function () { return types_1.PaymentProvider; } });
/**
* Nigerian NUBAN Account Validator
* @class Nuban
* @description Validates Nigerian bank account numbers using
* NUBAN algorithm and bank APIs
*/
class Nuban {
/**
* Creates an instance of NUBAN validator
* @param apiKey - The API key for the payment provider (Paystack or Flutterwave)
* @param paymentProvider - The payment provider to use (PAYSTACK or FLUTTERWAVE)
* @throws Will throw an error if apiKey is empty or payment provider is invalid
*
* @example
* ```typescript
* // Initialize with Paystack
* const nuban = new Nuban('sk_test_...', PaymentProvider.PAYSTACK);
*
* // Initialize with Flutterwave
* const nuban = new Nuban('FLWSECK_TEST-...', PaymentProvider.FLUTTERWAVE);
* ```
*/
constructor(apiKey, paymentProvider) {
if (!apiKey || !paymentProvider) {
throw new Error("API Key and Payment Provider are required");
}
this.apiKey = apiKey;
this.paymentProvider = paymentProvider;
}
/**
* Validates a Nigerian NUBAN account number
* @param accountNumber - The account number to validate (10 digits)
* @param bankCode - The bank code (e.g., "000015" for Zenith Bank)
* @returns Promise<AccountValidationResponse>
*
* @example
* ```typescript
* const nuban = new Nuban('your-api-key', 'PAYSTACK');
*
* // Validate account using Paystack
* const result = await nuban.validateAccount('0123456789', '057');
* // { status: true, message: 'Account found', data: { account_name: 'John Doe', ... } }
*
* // Invalid account number
* const invalid = await nuban.validateAccount('12345', '057');
* // { status: false, message: 'Invalid account number, digits should be exactly 10' }
* ```
*/
async validateAccount(accountNumber, bankCode) {
try {
if (!this.validateNuban(accountNumber)) {
return {
status: false,
message: "Invalid account number, digits should be exactly 10",
};
}
if (!this.validateBankCode(bankCode)) {
return {
status: false,
message: `Invalid bank code for payment provider - ${this.paymentProvider}`,
};
}
const queryMethod = {
[types_1.PaymentProvider.PAYSTACK]: this.paystackQuery,
[types_1.PaymentProvider.FLUTTERWAVE]: this.flutterwaveQuery,
}[this.paymentProvider];
if (!queryMethod) {
return {
status: false,
message: "Unsupported payment provider",
};
}
return await queryMethod.call(this, accountNumber, bankCode);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
status: false,
message: `Account validation failed - ${errorMessage}`,
};
}
}
/**
* Resolves card BIN (Bank Identification Number) information
* @param firstSixDigits - First 6 digits of the card number
* @returns Promise<CardBinResponse> Card BIN information including brand, type, and issuing bank
*
* @throws Will throw an error if the card BIN resolution fails
*
* @example
* ```typescript
* const nuban = new Nuban('your-api-key', PaymentProvider.PAYSTACK);
*
* // Resolve card BIN
* const cardInfo = await nuban.resolveCardBin('123456');
* // {
* // status: true,
* // message: 'Card BIN resolved',
* // data: {
* // bin: '123456',
* // brand: 'VISA',
* // card_type: 'DEBIT',
* // bank: 'Access Bank'
* // }
* // }
* ```
*/
async resolveCardBin(firstSixDigits) {
try {
// Validate input
if (!(firstSixDigits === null || firstSixDigits === void 0 ? void 0 : firstSixDigits.match(/^\d{6}$/))) {
return {
status: false,
message: "Invalid card BIN - must be exactly 6 digits",
};
}
const url = `${constants_1.CARD_BIN_URL[this.paymentProvider]}${firstSixDigits}`;
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const resolvedResponse = await response.json();
return this.paymentProvider === types_1.PaymentProvider.FLUTTERWAVE
? this.formatFlutterwaveCardBinResponse(resolvedResponse)
: {
...resolvedResponse,
data: {
...resolvedResponse.data,
brand: resolvedResponse.data.brand.toUpperCase(),
},
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
status: false,
message: `Card bin resolution failed - ${errorMessage}`,
};
}
}
/**
* Gets all possible Nigerian banks that could have issued a NUBAN account number
* based on CBN's NUBAN algorithm standard
* @see {@link 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 CBN NUBAN Standard}
*
* @param accountNumber - The 10-digit NUBAN account number to validate
* @param banks - Optional array of banks to check against (defaults to all Nigerian banks)
* @returns Array of banks that could have issued the account number
*
* @example
* ```typescript
* // Get all possible banks for an account number
* const possibleBanks = Nuban.getPossibleNubanBanks('0123456789');
* // [{ name: 'Access Bank', code: '000014' }, { name: 'Zenith Bank', code: '000015' }]
* ```
*
* Because of the irregularities in the NUBAN structure, a
* request to check a bank account can result in alot of banks
* returned, as I have 558 banks registered. So to streamline
* this for your use case you can pass custom banks to check.
*
* To this end I provided a default weightedBanks list that
* contains the most popular banks in the country that can be
* used to streammline your response to those banks.
* You can also pass custom banks.
*
* The optional weight param in the banks type is for you to
* filter the results by relevance to your specific need.
* @example
* ```typescript
* // Check against the default weighted banks
* const specificBanks = Nuban.getPossibleNubanBanks('0123456789', Nuban.weightedBanks);
*
* // Check against custom banks
* const specificBanks = Nuban.getPossibleNubanBanks('0123456789', [
* { id: '1', slug: '000014', name: 'Access Bank', code: '000014', weight: 1 }
* ]);
* ```
*/
static getPossibleNubanBanks(accountNumber, banks = Nuban.banks) {
return banks.filter((bank) => Nuban.isPossibleNubanBank(accountNumber, bank.code));
}
/**
* Get bank from slug, code or oldCode.
* @param value - The value to find (e.g., '000013')
* @param property - The bank property (e.g., BankProperty.CODE)
* @returns Bank
*
* @example
* ```typescript
* const bank = getBank('000013', BankProperty.CODE);
* // {
* // id: 11;
* // slug: 'bank_slug'
* // name: 'bank_name',
* // code: '000013'
* // }
* ```
*/
static getBank(value, property) {
const bankSearchMap = {
[types_1.BankProperty.SLUG]: (bank) => bank.slug === value,
[types_1.BankProperty.CODE]: (bank) => bank.code === value,
[types_1.BankProperty.OLD_CODE]: (bank) => bank.oldCode === value, // Only applies to weightedBanks
};
return property === types_1.BankProperty.OLD_CODE
? this.weightedBanks.find(bankSearchMap[property])
: this.banks.find(bankSearchMap[property]);
}
/**
* Makes an API request to Paystack to validate account details
* @param accountNumber - The account number to validate
* @param bankCode - The bank code to validate against
* @returns Promise<AccountValidationResponse>
* @throws Will throw an error if the API request fails
* @private
*/
async paystackQuery(accountNumber, bankCode) {
const url = `${constants_1.VALIDATION_URL[this.paymentProvider]}?account_number=${accountNumber}&bank_code=${bankCode}`;
try {
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return this.validateApiResponse(data);
}
catch (error) {
throw new Error(`Paystack API error: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Makes an API request to Flutterwave to validate account details
* @param accountNumber - The account number to validate
* @param bankCode - The bank code to validate against
* @throws Will throw an error if the API request fails
* @private
*/
async flutterwaveQuery(accountNumber, bankCode) {
const url = constants_1.VALIDATION_URL[this.paymentProvider];
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
account_number: accountNumber,
account_bank: bankCode,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return this.validateApiResponse(data);
}
catch (error) {
throw new Error(`Flutterwave API error: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Validates NUBAN format
* @param accountNumber - The account number to validate
* @returns boolean
*/
validateNuban(accountNumber) {
return Boolean((accountNumber === null || accountNumber === void 0 ? void 0 : accountNumber.length) === constants_1.NUBAN_LENGTH && /^\d+$/.test(accountNumber));
}
/**
* Validates bank code format based on payment provider
* @param bankCode - The bank code to validate
* @returns boolean indicating if the bank code format is valid
* @private
*
* @example
* ```typescript
* // Paystack (exactly 3 digits)
* // Requires the old bank code
* validateBankCode('057', PaymentProvider.PAYSTACK); // true
* validateBankCode('000015', PaymentProvider.PAYSTACK); // false
*
* // Flutterwave (accepts 3 or 6 digits)
* // Accepts both the old and new bank codes.
* validateBankCode('057', PaymentProvider.FLUTTERWAVE); // true
* validateBankCode('000015', PaymentProvider.FLUTTERWAVE); // true
* ```
*/
validateBankCode(bankCode) {
if (!(bankCode === null || bankCode === void 0 ? void 0 : bankCode.length) || !/^\d+$/.test(bankCode)) {
return false;
}
switch (this.paymentProvider) {
case types_1.PaymentProvider.PAYSTACK:
return bankCode.length === 3;
case types_1.PaymentProvider.FLUTTERWAVE:
return bankCode.length === 3 || bankCode.length === 6;
default:
return false;
}
}
/**
* Formats Flutterwave's card BIN response to match the standard CardBinResponse interface
* @param response - Raw response from Flutterwave's card BIN API
* @returns CardBinResponse Formatted card BIN information
*
* @example
* ```typescript
* // Raw Flutterwave response
* const rawResponse = {
* status: true,
* message: "success",
* data: {
* issuing_country: "NIGERIA NG",
* bin: "123456",
* card_type: "DEBIT",
* issuer_info: "VISA Access Bank"
* }
* };
*
* const formatted = this.formatFlutterwaveCardBinResponse(rawResponse);
* // {
* // status: true,
* // message: "success",
* // data: {
* // bin: "123456",
* // country_code: "NG",
* // country_name: "NIGERIA",
* // card_type: "DEBIT",
* // brand: "VISA",
* // bank: "Access Bank"
* // }
* // }
* ```
*/
formatFlutterwaveCardBinResponse(response) {
const [country_name, country_code] = response.data.issuing_country.split(" ");
const [brand, ...bankParts] = response.data.issuer_info.split(" ");
const formattedResponse = {
...response,
data: {
bin: response.data.bin,
country_code,
country_name,
card_type: response.data.card_type,
brand: brand,
bank: bankParts.join(" "),
},
};
return formattedResponse;
}
/**
* Validates and type checks the API response
* @param response - Raw API response
* @returns AccountValidationResponse
* @throws Will throw an error if the response format is invalid
* @private
*/
validateApiResponse(response) {
if (typeof response === "object" &&
response !== null &&
"status" in response &&
"message" in response) {
return response;
}
throw new Error("Invalid API response format");
}
/**
* Generates a seed array for NUBAN validation based on CBN's algorithm
* @param length - Length of the seed array to generate
* @returns Array of alternating numbers (3,7) based on the specified length
* @private
*
* @example
* [3, 7, 3, 3, 7, 3, 3, 7, 3]
*/
static generateSeed(length) {
return Array.from({ length }, (_, i) => (i % 2 ? 7 : 3));
}
/**
* Validates the NUBAN serial code format
* @param nubanSerialCode - 9-digit NUBAN serial code
* @throws {Error} If serial code is invalid or exceeds maximum length
* @returns true if the serial code is valid
* @private
*/
static validateNubanSerialCode(nubanSerialCode) {
if (!nubanSerialCode || nubanSerialCode.length > constants_1.NUBAN_SERIAL_CODE_LENGTH) {
throw new Error(`Nuban serial code should not be more than ${constants_1.NUBAN_SERIAL_CODE_LENGTH} digits`);
}
return true;
}
/**
* Generates the check digit for a NUBAN account number using CBN's algorithm
* @see {@link 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 CBN NUBAN Standard}
* @param nubanSerialCode - First 9 digits of the account number
* @param bankCode - 6-digit bank code
* @returns The check digit (last digit) of the NUBAN
* @private
*
* @example
* ```typescript
* const checkDigit = Nuban.generateCheckDigit('123456789', '000014');
* // Returns a number between 0-9
* ```
*/
static generateCheckDigit(nubanSerialCode, bankCode) {
this.validateNubanSerialCode(nubanSerialCode);
const paddedCode = nubanSerialCode.padStart(constants_1.NUBAN_SERIAL_CODE_LENGTH, "0");
const crypto = bankCode + paddedCode;
const seed = this.generateSeed(crypto.length);
// 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+M*3+N*7+O*3.
const cryptoSum = crypto
.split("")
.reduce((sum, digit, index) => sum + seed[index] * Number(digit), 0);
// Step 2: Calculate Modulo 10 of your result i.e. the remainder after dividing by 10
const modulo = cryptoSum % 10;
// Step 3. Subtract your result from 10 to get the Check Digit.
// Step 4. Subtract 1 to get Check Digit 9.
return 10 - modulo - 1;
}
/**
* Checks if a bank could have issued a specific NUBAN account number
* @param accountNumber - Complete 10-digit NUBAN account number
* @param bankCode - 6-digit bank code to validate against
* @returns boolean indicating if the bank could have issued the account number
* @private
*
* @example
* ```typescript
* const isValid = Nuban.isPossibleNubanBank('0123456789', '000014');
* // Returns true if the check digit matches the bank's algorithm
* ```
*/
static isPossibleNubanBank(accountNumber, bankCode) {
const nubanSerialCode = accountNumber.substring(0, 9);
const checkDigit = this.generateCheckDigit(nubanSerialCode, bankCode);
return checkDigit === Number(accountNumber[9]);
}
}
exports.Nuban = Nuban;
Nuban.banks = banks_1.NIGERIAN_BANKS;
Nuban.weightedBanks = weighted_bank_1.WEIGHTED_NIGERIAN_BANKS;