finnish-bank-utils
Version:
A micro Javascript library for validating, creating and formatting Finnish IBAN bank account numbers and reference numbers. Can parse payment details from Finnish virtual barcodes.
369 lines (309 loc) • 12.8 kB
JavaScript
(function (global, factory) {
if (typeof define === "function" && define.amd) {
define('FinnishBankUtils', ['module'], factory);
} else if (typeof exports !== "undefined") {
factory(module);
} else {
var mod = {
exports: {}
};
factory(mod);
global.FinnishBankUtils = mod.exports;
}
})(this, function (module) {
'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
var _slicedToArray = function () {
function sliceIterator(arr, i) {
var _arr = [];
var _n = true;
var _d = false;
var _e = undefined;
try {
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"]) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
return function (arr, i) {
if (Array.isArray(arr)) {
return arr;
} else if (Symbol.iterator in Object(arr)) {
return sliceIterator(arr, i);
} else {
throw new TypeError("Invalid attempt to destructure non-iterable instance");
}
};
}();
function _toConsumableArray(arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {
arr2[i] = arr[i];
}
return arr2;
} else {
return Array.from(arr);
}
}
var REF_NUMBER_MULTIPLIERS = [7, 3, 1],
FINNISH_REF_NUMBER_REGEX = /^(\d{4,20}|RF\d{6,22})$/i,
FINNISH_IBAN_REGEX = /^FI\d{16}$/,
FINNISH_VIRTUAL_BAR_CODE_REGEX = /^[45]\d{53}$/,
FINNISH_DATE_REGEX = /^(\d\d?)\.(\d\d?)\.(\d{4})$/,
IBAN_OFFSET_FROM_ASCIICODE = -55;
function removeAllWhiteSpaces(str) {
return str.replace(/\s+/g, '');
}
function removeLeadingZeros(str) {
return str.replace(/^0+/, '');
}
function lettersToNumbers(str) {
return [].concat(_toConsumableArray(str)).map(function (char) {
if (/\D/.test(char)) {
return String(char.charCodeAt(0) + IBAN_OFFSET_FROM_ASCIICODE);
}
return char;
}).join('');
}
function reverseString(str) {
return [].concat(_toConsumableArray(str)).reverse().join('');
}
function removeStringFromEnd(str, strToRemove) {
if (str.substr(-strToRemove.length) === strToRemove) {
return str.substr(0, str.length - strToRemove.length);
}
return str;
}
function leftPadString(str, char, pad) {
return (char.repeat(pad) + str).substr(-Math.max(str.length, pad));
}
function randomNumberWithLength(length) {
var randomNumber = '';
for (var i = 0; i < length; i++) {
randomNumber += Math.floor(Math.random() * 9) + 1; // 1...9, because a real number can't begin with zero
}
return parseInt(randomNumber, 10);
}
/** JS number type can't handle the long account numbers... */
function modForLargeNumber(base, divisor) {
var dividend = '';
for (var i = 0; i < base.length; i++) {
dividend = parseInt(dividend + base[i], 10);
if (dividend >= divisor) {
var remainder = dividend % divisor;
if (i == base.length - 1) {
return remainder;
} else {
dividend = remainder;
}
}
}
return parseInt(dividend, 10);
}
/** Luhn mod 10 checksum algorithm https://en.wikipedia.org/wiki/Luhn_algorithm */
function luhnMod10(value) {
var sum = 0;
for (var i = 0; i < value.length; i++) {
var multiplier = i % 2 === 0 ? 2 : 1;
var add = multiplier * parseInt(value[i], 10);
if (add >= 10) {
add -= 9;
}
sum += add;
}
var mod10 = sum % 10;
return mod10 === 0 ? mod10 : 10 - mod10;
}
function isValidFinnishBBAN(accountNumber) {
accountNumber = removeAllWhiteSpaces(accountNumber);
var localAccountNumberWithoutCheckSum = accountNumber.substr(4, 13),
luhnChecksumChar = parseInt(accountNumber.substr(17, 1), 10);
return luhnMod10(localAccountNumberWithoutCheckSum) === luhnChecksumChar;
}
function isValidIBAN(iban) {
iban = removeAllWhiteSpaces(iban.toUpperCase());
var prefixAndChecksum = iban.substr(0, 4),
number = iban.substr(4);
return modForLargeNumber(lettersToNumbers(number + prefixAndChecksum), 97) === 1;
}
function isValidFinnishDate(string) {
if (!string || typeof string != 'string' || !FINNISH_DATE_REGEX.test(string)) {
return false;
}
var _string$match$slice$m = string.match(FINNISH_DATE_REGEX).slice(1, 4).map(Number),
_string$match$slice$m2 = _slicedToArray(_string$match$slice$m, 3),
day = _string$match$slice$m2[0],
month = _string$match$slice$m2[1],
year = _string$match$slice$m2[2],
date = new Date(year, month - 1, day);
return year == date.getFullYear() && month - 1 == date.getMonth() && day == date.getDate();
}
function sliceVirtualBarCode(barCode) {
var version = Number(barCode.substr(0, 1));
var slices = void 0;
if (version === 4) {
slices = [1, 16, 6, 2, 3, 20, 2, 2, 2];
} else if (version === 5) {
slices = [1, 16, 6, 2, 0, 23, 2, 2, 2];
}
var index = 0;
return slices.map(function (length) {
var slice = barCode.substr(index, length);
index += length;
return slice;
});
}
var FinnishBankUtils = {
isValidFinnishRefNumber: function isValidFinnishRefNumber(refNumber) {
// Sanity and format check, which allows to make safe assumptions on the format.
if (!refNumber || typeof refNumber !== 'string' || !FINNISH_REF_NUMBER_REGEX.test(removeAllWhiteSpaces(refNumber.toUpperCase()))) {
return false;
}
refNumber = removeAllWhiteSpaces(refNumber.toUpperCase());
if (/^RF/.test(refNumber)) {
if (!isValidIBAN(refNumber)) {
return false;
}
refNumber = refNumber.substr(4);
} else {
refNumber = removeLeadingZeros(refNumber);
}
var reversedRefNumber = reverseString(refNumber),
providedChecksumNumber = parseInt(reversedRefNumber.charAt(0));
refNumber = reversedRefNumber.substr(1);
var checksum = 0,
checksumNumber = void 0;
for (var i = 0; i < refNumber.length; i++) {
checksum += REF_NUMBER_MULTIPLIERS[i % REF_NUMBER_MULTIPLIERS.length] * parseInt(refNumber.charAt(i));
}
checksumNumber = 10 - checksum % 10;
if (checksumNumber === 10) {
checksumNumber = 0;
}
return checksumNumber === providedChecksumNumber;
},
isValidFinnishIBAN: function isValidFinnishIBAN(accountNumber) {
if (!accountNumber || typeof accountNumber !== 'string' || !FINNISH_IBAN_REGEX.test(removeAllWhiteSpaces(accountNumber.toUpperCase()))) {
return false;
}
return isValidFinnishBBAN(accountNumber) && isValidIBAN(accountNumber);
},
formatFinnishRefNumber: function formatFinnishRefNumber(refNumber) {
var separator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ' ';
if (this.isValidFinnishRefNumber(refNumber)) {
refNumber = removeAllWhiteSpaces(refNumber.toUpperCase());
if (/^RF/.test(refNumber)) {
refNumber = refNumber.substr(0, 4) + removeLeadingZeros(refNumber.substr(4));
return removeStringFromEnd(refNumber.replace(/.{4}/g, '$&' + separator), separator);
} else {
refNumber = removeLeadingZeros(refNumber);
return reverseString(removeStringFromEnd(reverseString(refNumber).replace(/.{5}/g, '$&' + separator), separator));
}
}
},
formatFinnishIBAN: function formatFinnishIBAN(accountNumber) {
var separator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ' ';
if (this.isValidFinnishIBAN(accountNumber)) {
accountNumber = removeAllWhiteSpaces(accountNumber.toUpperCase());
return removeStringFromEnd(accountNumber.replace(/.{4}/g, '$&' + separator), separator);
}
},
generateFinnishRefNumber: function generateFinnishRefNumber(initial) {
var refNumber = typeof initial === 'string' ? removeAllWhiteSpaces(initial) : randomNumberWithLength(9).toString(),
reversedRefNumber = reverseString(refNumber);
var checksum = 0,
checksumNumber = void 0;
for (var i = 0; i < reversedRefNumber.length; i++) {
checksum += REF_NUMBER_MULTIPLIERS[i % REF_NUMBER_MULTIPLIERS.length] * parseInt(reversedRefNumber.charAt(i));
}
checksumNumber = 10 - checksum % 10;
if (checksumNumber === 10) {
checksumNumber = 0;
}
return refNumber + checksumNumber;
},
generateFinnishIBAN: function generateFinnishIBAN() {
var defaultCheckDigit = '00',
danskeBankOffice = '800026',
// Use a real bank and office for simplicity
countryCodeInDigits = lettersToNumbers('FI'),
bankAccount = randomNumberWithLength(7),
localAccountNumber = danskeBankOffice + bankAccount + luhnMod10(danskeBankOffice + bankAccount),
accountNumberCandidate = localAccountNumber + countryCodeInDigits + defaultCheckDigit,
checkDigit = 98 - modForLargeNumber(accountNumberCandidate, 97),
checkChars = checkDigit >= 10 ? checkDigit.toString() : '0' + checkDigit;
return 'FI' + checkChars + localAccountNumber;
},
parseFinnishVirtualBarCode: function parseFinnishVirtualBarCode(barCode) {
if (!barCode || typeof barCode != 'string' || !FINNISH_VIRTUAL_BAR_CODE_REGEX.test(barCode)) {
return false;
}
var _sliceVirtualBarCode = sliceVirtualBarCode(barCode),
_sliceVirtualBarCode2 = _slicedToArray(_sliceVirtualBarCode, 9),
version = _sliceVirtualBarCode2[0],
iban = _sliceVirtualBarCode2[1],
euros = _sliceVirtualBarCode2[2],
cents = _sliceVirtualBarCode2[3],
reserve = _sliceVirtualBarCode2[4],
reference = _sliceVirtualBarCode2[5],
year = _sliceVirtualBarCode2[6],
month = _sliceVirtualBarCode2[7],
day = _sliceVirtualBarCode2[8];
version = Number(version);
iban = this.formatFinnishIBAN('FI' + iban);
var sum = Number(euros) + Number(cents) / 100;
if (version === 5) {
reference = 'RF' + reference.substr(0, 2) + removeLeadingZeros(reference.substr(2));
}
reference = this.formatFinnishRefNumber(reference);
var date = void 0;
day = Number(day);
month = Number(month);
if (day > 0 && month > 0) {
date = day + '.' + month + '.20' + year;
}
return { iban: iban, sum: sum, reference: reference, date: date };
},
formatFinnishVirtualBarCode: function formatFinnishVirtualBarCode(object) {
if (!object || (typeof object === 'undefined' ? 'undefined' : _typeof(object)) != 'object' || !this.isValidFinnishIBAN(object.iban) || typeof object.sum != 'number' || object.sum < 0 || object.sum > 999999.99 || object.sum != Number(object.sum.toFixed(2)) || !this.isValidFinnishRefNumber(object.reference) || object.date != undefined && !isValidFinnishDate(object.date)) {
return false;
}
var iban = removeAllWhiteSpaces(object.iban),
euros = Math.floor(object.sum),
cents = object.sum * 100 - euros * 100,
reference = removeAllWhiteSpaces(object.reference),
day = 0,
month = 0,
year = 0,
version = /^RF/.test(reference) ? 5 : 4;
if (object.date) {
var _object$date$match$sl = object.date.match(FINNISH_DATE_REGEX).slice(1, 4).map(Number);
var _object$date$match$sl2 = _slicedToArray(_object$date$match$sl, 3);
day = _object$date$match$sl2[0];
month = _object$date$match$sl2[1];
year = _object$date$match$sl2[2];
}
if (version == 5) {
reference = reference.replace(/^RF/, '');
reference = reference.substr(0, 2) + leftPadString(reference.substr(2), '0', 21);
}
return String(version) + iban.replace(/^FI/, '') + leftPadString(String(euros), '0', 6) + leftPadString(String(cents), '0', 2) + leftPadString(reference, '0', 23) + leftPadString(String(year).substr(-2), '0', 2) + leftPadString(String(month), '0', 2) + leftPadString(String(day), '0', 2);
}
};
module.exports = Object.freeze(FinnishBankUtils);
});