@usecomma/modulus-check
Version:
Validate a UK bank account number against a sort code using the VocaLink modulus check
320 lines (246 loc) • 8.34 kB
JavaScript
/**
* Module dependencies.
*/
import { positions } from './constants';
import fs from 'fs';
/**
* Export UkModulusChecking.
*/
export default class UkModulusChecking {
/**
* Constructor.
*/
constructor({ accountNumber = '', sortCode = '' }) {
this.accountNumber = this.sanitize(accountNumber);
this.sortCode = this.sanitize(sortCode);
this.sortCodeSubstitutes = this.loadScsubtab();
this.weightTable = this.loadValacdos();
}
/**
* Get check weight.
*/
getCheckWeight(check, number) {
if (check.exception === 2) {
if (this.pickPosition(number, 'a') !== 0 && this.pickPosition(number, 'g') !== 9) {
return [0, 0, 1, 2, 5, 3, 6, 4, 8, 7, 10, 9, 3, 1];
}
if (this.pickPosition(number, 'a') !== 0 && this.pickPosition(number, 'g') === 9) {
return [0, 0, 0, 0, 0, 0, 0, 0, 8, 7, 10, 9, 3, 1];
}
}
if (check.exception === 7) {
if (this.pickPosition(number, 'g') === 9) {
return [0, 0, 0, 0, 0, 0, 0, 0, check.c, check.d, check.e, check.f, check.g, check.h];
}
}
if (check.exception === 10) {
const ab = number.charAt(positions.a) + number.charAt(positions.b);
if (ab === '09' || ab === '99' && this.pickPosition(number, 'b') === 9) {
return [0, 0, 0, 0, 0, 0, 0, 0, check.c, check.d, check.e, check.f, check.g, check.h];
}
}
return [check.u, check.v, check.w, check.x, check.y, check.z, check.a, check.b, check.c, check.d, check.e, check.f, check.g, check.h];
}
/**
* Get number to be used in validation process. (sorting code + account number).
*/
getNumber(check, number) {
let sortCode = this.sortCode;
number = number || this.accountNumber;
if (check.exception === 5) {
sortCode = this.getSubstitute(sortCode) || sortCode;
} else if (check.exception === 8) {
sortCode = '090126';
} else if (check.exception === 9) {
sortCode = '309634';
}
return `${sortCode}${number}`;
}
/**
* Get sorting code checks.
*/
getSortCodeChecks() {
const checks = [];
const sortCode = parseInt(this.sortCode, 10);
for (const check of this.weightTable) {
// All checks containing the sort code in the `weight range` can/must be performed.
if (sortCode >= check.start && sortCode <= check.end) {
checks.push(check);
}
// There may be one or two entries in the table for the sorting code,
// depending on whether one or two modulus checks must be carried out.
if (checks.length === 2) {
return checks;
}
}
return checks;
}
/**
* Sorting code substitution.
*/
getSubstitute(sortCode) {
for (const substitute of this.sortCodeSubstitutes) {
if (substitute.original === parseInt(sortCode, 10)) {
return parseInt(substitute.substitute, 10);
}
}
return parseInt(sortCode, 10);
}
/**
* Is check skippable.
*/
isCheckSkippable(check, number) {
if (check.exception === 3 && (this.pickPosition(number, 'c') === 6 || this.pickPosition(number, 'c') === 9)) {
return true;
}
if (check.exception === 6 && this.pickPosition(number, 'a') >= 4 && this.pickPosition(number, 'a') <= 8 && this.pickPosition(number, 'g') === this.pickPosition(number, 'h')) {
return true;
}
return false;
}
/**
* Is check valid.
*/
isCheckValid(check, number) {
number = this.getNumber(check, number);
if (this.isCheckSkippable(check, number)) {
return true;
}
const module = check.mod === 'MOD11' ? 11 : 10;
const weight = this.getCheckWeight(check, number);
// Multiply each number in the sorting code and account number with the corresponding number in the weight.
let weightedAccount = [];
for (let i = 0; i < 14; i++) {
weightedAccount[i] = parseInt(number.charAt(i), 10) * parseInt(weight[i], 10);
}
// Add all the results together.
if (check.mod === 'DBLAL') {
weightedAccount = weightedAccount.join('').split('');
}
let total = weightedAccount.reduce((previous, current) => parseInt(previous, 10) + parseInt(current, 10));
// This effectively places a financial institution number (580149) before the sorting code and account
// number which is subject to the alternate doubling as well.
if (check.exception === 1) {
total += 27;
}
// Calculate remainder.
const remainder = total % module;
// Exception handling.
if (check.exception === 4) {
return remainder === this.pickPosition(number, 'g') + this.pickPosition(number, 'h');
}
if (check.exception === 5) {
if (check.mod === 'DBLAL') {
if (remainder === 0 && this.pickPosition(number, 'h') === 0) {
return true;
}
return this.pickPosition(number, 'h') === 10 - remainder;
}
if (remainder === 1) {
return false;
}
if (remainder === 0 && this.pickPosition(number, 'g') === 0) {
return true;
}
return this.pickPosition(number, 'g') === 11 - remainder;
}
return remainder === 0;
}
/**
* Is valid.
*/
isValid() {
if (this.accountNumber.length < 6 || this.accountNumber.length > 10 || this.sortCode.length !== 6) {
return false;
}
const checks = this.getSortCodeChecks();
// If no range is found that contains the sorting code, there is no modulus check that can be performed.
// The sorting code and account number should be presumed valid unless other evidence implies otherwise.
if (checks.length === 0) {
return true;
}
const firstCheck = checks[0];
if (this.isCheckValid(firstCheck)) {
if (checks.length === 1 || [2, 9, 10, 11, 12, 13, 14].indexOf(firstCheck.exception) !== -1) {
return true;
}
// Verify second check.
return this.isCheckValid(checks[1]);
}
if (firstCheck.exception === 14) {
if ([0, 1, 9].indexOf(parseInt(this.accountNumber.charAt(7), 10)) === -1) {
return false;
}
// If the 8th digit is 0, 1 or 9, then remove the digit from the account number and insert a 0 as the 1st digit for check purposes only
return this.isCheckValid(checks[0], `0${this.accountNumber.substring(7, 0)}`);
}
if (checks.length === 1 || [2, 9, 10, 11, 12, 13, 14].indexOf(firstCheck.exception) === -1) {
return false;
}
// Verify second check.
return this.isCheckValid(checks[1]);
}
/**
* Load scsubtab file.
*/
loadScsubtab() {
const content = fs.readFileSync(`${__dirname}/data/scsubtab.txt`, 'utf8');
const scsubtab = [];
content.split('\r\n').forEach((line) => {
const data = line.split(/\s+/);
scsubtab.push({
original: parseInt(data[0], 10),
substitute: parseInt(data[1], 10)
});
});
return scsubtab;
}
/**
* Load valacdos file.
*/
loadValacdos() {
const content = fs.readFileSync(`${__dirname}/data/valacdos-v7-40.txt`, 'utf8');
const valacdos = [];
content.split('\r\n').forEach((line) => {
const data = line.split(/\s+/);
/* jscs:disable validateOrderInObjectKeys */
valacdos.push({
start: parseInt(data[0], 10),
end: parseInt(data[1], 10),
mod: data[2],
u: parseInt(data[3], 10),
v: parseInt(data[4], 10),
w: parseInt(data[5], 10),
x: parseInt(data[6], 10),
y: parseInt(data[7], 10),
z: parseInt(data[8], 10),
a: parseInt(data[9], 10),
b: parseInt(data[10], 10),
c: parseInt(data[11], 10),
d: parseInt(data[12], 10),
e: parseInt(data[13], 10),
f: parseInt(data[14], 10),
g: parseInt(data[15], 10),
h: parseInt(data[16], 10),
exception: parseInt(data[17], 10) || null
});
/* jscs:enable validateOrderInObjectKeys */
});
return valacdos;
}
/**
* Pick position in number.
*/
pickPosition(number, position) {
return parseInt(number.charAt(positions[position]), 10);
}
/**
* Sanitize.
*/
sanitize(value) {
if (typeof value === 'string' || value instanceof String) {
return value.replace(/-/g, '');
}
throw new Error('Invalid value');
}
}