zellige.js
Version:
A Moroccan utility library for working with CIN, phone numbers, currency, addresses, dates, and more.
311 lines (310 loc) • 12 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BankValidationException = exports.BankValidationError = void 0;
exports.isValidIBAN = isValidIBAN;
exports.isValidRIB = isValidRIB;
exports.getBankDetails = getBankDetails;
exports.getSwiftCode = getSwiftCode;
exports.madToWords = madToWords;
const banks_1 = require("../constants/banks");
// Add error codes enum at the top of the file
var BankValidationError;
(function (BankValidationError) {
BankValidationError["INVALID_INPUT_TYPE"] = "BANK_001";
BankValidationError["INVALID_IBAN_FORMAT"] = "BANK_002";
BankValidationError["INVALID_BANK_CODE"] = "BANK_003";
BankValidationError["INVALID_IBAN_CHECKSUM"] = "BANK_004";
BankValidationError["INVALID_RIB_FORMAT"] = "BANK_005";
BankValidationError["INVALID_RIB_LENGTH"] = "BANK_006";
BankValidationError["INVALID_RIB_CHECKSUM"] = "BANK_007";
BankValidationError["BANK_NOT_FOUND"] = "BANK_008";
BankValidationError["INVALID_AMOUNT"] = "BANK_009";
BankValidationError["BRANCH_NOT_FOUND"] = "BANK_010";
})(BankValidationError || (exports.BankValidationError = BankValidationError = {}));
// Custom error class
class BankValidationException extends Error {
constructor(code, message, details) {
super(message);
this.code = code;
this.details = details;
this.name = 'BankValidationException';
}
}
exports.BankValidationException = BankValidationException;
/**
* Validates a given IBAN (International Bank Account Number) for Moroccan banks.
*
* @param iban - The IBAN string to validate.
* @returns `true` if the IBAN is valid, `false` otherwise.
* @throws {TypeError} If the provided IBAN is not a string.
*
* The function performs the following checks:
* 1. Ensures the IBAN is a string.
* 2. Removes any whitespace and converts the IBAN to uppercase.
* 3. Checks if the IBAN matches the Moroccan IBAN format (starts with 'MA' followed by 26 digits).
* 4. Extracts the bank code and verifies it against a list of active Moroccan banks.
* 5. Rearranges the IBAN and converts it to a numeric string for the MOD 97-10 check.
* 6. Performs the MOD 97-10 check to validate the IBAN.
*/
function isValidIBAN(iban) {
try {
if (typeof iban !== 'string') {
throw new BankValidationException(BankValidationError.INVALID_INPUT_TYPE, 'IBAN must be a string', { providedType: typeof iban });
}
const cleanIBAN = iban.replace(/\s/g, '').toUpperCase();
if (!/^MA\d{26}$/.test(cleanIBAN)) {
throw new BankValidationException(BankValidationError.INVALID_IBAN_FORMAT, 'Invalid IBAN format', { iban: cleanIBAN });
}
const bankCode = cleanIBAN.substring(4, 7);
const bank = banks_1.MOROCCAN_BANKS.find(b => b.code === bankCode && b.active);
if (!bank) {
throw new BankValidationException(BankValidationError.INVALID_BANK_CODE, 'Invalid or inactive bank code', { bankCode });
}
if (!bank.ibanRegex.test(cleanIBAN)) {
throw new BankValidationException(BankValidationError.INVALID_IBAN_FORMAT, 'IBAN does not match bank-specific format', { bankCode, iban: cleanIBAN });
}
const rearranged = cleanIBAN.slice(4) + cleanIBAN.slice(0, 4);
const numeric = rearranged
.split('')
.map(c => (/\d/.test(c) ? c : (c.charCodeAt(0) - 55).toString()))
.join('');
let remainder = 0;
for (let i = 0; i < numeric.length; i++) {
remainder = (remainder * 10 + parseInt(numeric[i])) % 97;
}
if (remainder !== 1) {
throw new BankValidationException(BankValidationError.INVALID_IBAN_CHECKSUM, 'Invalid IBAN checksum', { iban: cleanIBAN });
}
return true;
}
catch (error) {
if (error instanceof BankValidationException) {
// You can log the error here if needed
console.error(`Bank validation error: ${error.code} - ${error.message}`);
}
return false;
}
}
/**
* Validates a Moroccan RIB (Relevé d'Identité Bancaire).
*
* This function checks if the provided RIB is valid by performing the following steps:
* 1. Cleans the RIB by removing spaces and hyphens.
* 2. Extracts the bank code and verifies it against a list of active Moroccan banks.
* 3. Checks the length and format of the RIB against bank-specific rules.
* 4. Calculates the expected key using the MOD 97-10 algorithm and compares it with the actual key.
*
* @param rib - The RIB string to validate.
* @returns `true` if the RIB is valid, `false` otherwise.
*/
function isValidRIB(rib) {
try {
const cleanRIB = rib.replace(/[\s-]/g, '');
const bankCode = cleanRIB.substring(0, 3);
const bank = banks_1.MOROCCAN_BANKS.find(b => b.code === bankCode && b.active);
if (!bank) {
throw new BankValidationException(BankValidationError.BANK_NOT_FOUND, 'Invalid or inactive bank code', { bankCode });
}
if (cleanRIB.length !== bank.ribLength) {
throw new BankValidationException(BankValidationError.INVALID_RIB_LENGTH, 'Invalid RIB length', {
expected: bank.ribLength,
received: cleanRIB.length,
});
}
if (!bank.ribRegex.test(cleanRIB)) {
throw new BankValidationException(BankValidationError.INVALID_RIB_FORMAT, 'RIB does not match bank-specific format', { bankCode, rib: cleanRIB });
}
const payload = cleanRIB.slice(0, -2);
const actualKey = parseInt(cleanRIB.slice(-2), 10);
const chunks = [];
for (let i = 0; i < payload.length; i += 7) {
chunks.push(parseInt(payload.slice(i, i + 7), 10));
}
let remainder = 0;
for (const chunk of chunks) {
remainder =
(remainder * Math.pow(10, chunk.toString().length) + chunk) % 97;
}
const expectedKey = (97 - remainder) % 97;
if (expectedKey !== actualKey) {
throw new BankValidationException(BankValidationError.INVALID_RIB_CHECKSUM, 'Invalid RIB checksum', {
expected: expectedKey,
actual: actualKey,
rib: cleanRIB,
});
}
return true;
}
catch (error) {
if (error instanceof BankValidationException) {
console.error(`Bank validation error: ${error.code} - ${error.message}`);
}
return false;
}
}
function getBankDetails(code) {
try {
const cleanCode = code.replace(/[\s-]/g, '');
const bankCode = cleanCode.startsWith('MA')
? cleanCode.substring(4, 7)
: cleanCode.substring(0, 3);
const bank = banks_1.MOROCCAN_BANKS.find(b => b.code === bankCode && b.active);
if (!bank) {
throw new BankValidationException(BankValidationError.BANK_NOT_FOUND, 'Bank not found or inactive', { bankCode });
}
return bank;
}
catch (error) {
if (error instanceof BankValidationException) {
console.error(`Bank validation error: ${error.code} - ${error.message}`);
}
return undefined;
}
}
/**
* Gets SWIFT/BIC code with branch support
*/
function getSwiftCode(code, branch) {
try {
const bank = getBankDetails(code);
if (!bank) {
throw new BankValidationException(BankValidationError.BANK_NOT_FOUND, 'Bank not found', { code });
}
if (branch && bank.branches) {
const branchDetails = bank.branches.find(b => b.code === branch);
if (!branchDetails) {
throw new BankValidationException(BankValidationError.BRANCH_NOT_FOUND, 'Branch not found', { bankCode: code, branchCode: branch });
}
return branchDetails.swift;
}
return bank.swift;
}
catch (error) {
if (error instanceof BankValidationException) {
console.error(`Bank validation error: ${error.code} - ${error.message}`);
}
return undefined;
}
}
/**
* Converts a given amount in Moroccan Dirhams (MAD) to its French words representation.
*
* @param amount - The amount in Moroccan Dirhams to be converted.
* @returns The French words representation of the given amount.
*
* @example
* ```typescript
* madToWords(1234); // "mille deux cent trente-quatre dirhams"
* madToWords(0); // "zéro dirhams"
* madToWords(-45.67); // "moins quarante-cinq dirhams et soixante-sept centimes"
* ```
*/
function madToWords(amount) {
try {
if (typeof amount !== 'number' || isNaN(amount)) {
throw new BankValidationException(BankValidationError.INVALID_AMOUNT, 'Amount must be a valid number', { providedAmount: amount });
}
const units = [
'',
'un',
'deux',
'trois',
'quatre',
'cinq',
'six',
'sept',
'huit',
'neuf',
];
const teens = [
'dix',
'onze',
'douze',
'treize',
'quatorze',
'quinze',
'seize',
'dix-sept',
'dix-huit',
'dix-neuf',
];
const tens = [
'',
'dix',
'vingt',
'trente',
'quarante',
'cinquante',
'soixante',
'soixante-dix',
'quatre-vingt',
'quatre-vingt-dix',
];
const convertLessThanThousand = (n) => {
let result = '';
if (n >= 100) {
result += (n >= 200 ? units[Math.floor(n / 100)] + ' ' : '') + 'cent';
n %= 100;
if (n > 0)
result += ' ';
}
if (n >= 80 && n < 100) {
result += 'quatre-vingt' + (n === 80 ? '' : '-' + units[n - 80]);
}
else if (n >= 20) {
result += tens[Math.floor(n / 10)];
if (n % 10 > 0)
result += '-' + units[n % 10];
}
else if (n >= 10) {
result += teens[n - 10];
}
else if (n > 0) {
result += units[n];
}
return result;
};
if (amount === 0)
return 'zéro dirhams';
let result = '';
const isNegative = amount < 0;
amount = Math.abs(amount);
const whole = Math.floor(amount);
const cents = Math.round((amount - whole) * 100);
if (whole > 0) {
const millions = Math.floor(whole / 1000000);
const thousands = Math.floor((whole % 1000000) / 1000);
const remainder = whole % 1000;
if (millions > 0) {
result +=
convertLessThanThousand(millions) +
' million' +
(millions > 1 ? 's ' : ' ');
}
if (thousands > 0) {
result +=
thousands === 1
? 'mille '
: convertLessThanThousand(thousands) + ' mille ';
}
if (remainder > 0) {
result += convertLessThanThousand(remainder);
}
result += ' dirham' + (whole !== 1 ? 's' : '');
}
if (cents > 0) {
if (whole > 0)
result += ' et ';
result +=
convertLessThanThousand(cents) + ' centime' + (cents !== 1 ? 's' : '');
}
return (isNegative ? 'moins ' : '') + result.trim();
}
catch (error) {
if (error instanceof BankValidationException) {
console.error(`Bank validation error: ${error.code} - ${error.message}`);
return 'erreur de conversion';
}
return 'erreur de conversion';
}
}