uk-modulus-check
Advanced tools
Comparing version 1.0.16 to 2.0.0
{ | ||
"name": "uk-modulus-check", | ||
"version": "1.0.16", | ||
"version": "2.0.0", | ||
"main": "dist/index.js", | ||
@@ -5,0 +5,0 @@ "types": "dist/index.d.ts", |
@@ -27,10 +27,16 @@ # UKModulusCheck | ||
``` | ||
import ModulusChecker from "uk-modulus-check"; | ||
import {validateAccountDetails} from "uk-modulus-check"; | ||
const checker = new ModulusChecker(); | ||
console.log(checker.validate('180002', '00000190')); // true | ||
console.log(checker.validate('938063', '15763217')); // false | ||
console.log(validateAccountDetails('180002', '00000190')); // true | ||
console.log(validateAccountDetails('938063', '15763217')); // false | ||
``` | ||
``` | ||
const {validateAccountDetails} = require("uk-modulus-check"); | ||
console.log(validateAccountDetails('180002', '00000190')); // true | ||
console.log(validateAccountDetails('938063', '15763217')); // false | ||
``` | ||
## Details | ||
@@ -37,0 +43,0 @@ |
@@ -23,2 +23,2 @@ { | ||
"938654": "938621" | ||
} | ||
} |
import { readFileSync, writeFileSync } from 'fs'; | ||
const processSubstitutionMap = () => | ||
{ | ||
const scsubtab = readFileSync(`${__dirname}/data/scsubtab.txt`, 'utf8') | ||
.split('\r\n') | ||
.map((line) => line.split(/\s+/)) | ||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); | ||
const processSubstitutionMap = () => { | ||
const scsubtab = readFileSync(`${__dirname}/data/scsubtab.txt`, 'utf8') | ||
.split('\r\n') | ||
.map((line) => line.split(/\s+/)) | ||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); | ||
writeFileSync(`${__dirname}/data/scsubtab.json`, JSON.stringify(scsubtab, null, 2)); | ||
}; | ||
writeFileSync( | ||
`${__dirname}/data/scsubtab.json`, | ||
JSON.stringify(scsubtab, null, 2) | ||
); | ||
}; | ||
const processModulusWeights = () => { | ||
const valacdos = readFileSync(`${__dirname}/data/valacdos-v7-90.txt`, 'utf8') | ||
const valacdos = readFileSync(`${__dirname}/data/valacdos-v7-90.txt`, 'utf8') | ||
.split('\r\n') | ||
@@ -28,7 +29,9 @@ .map((line) => { | ||
writeFileSync(`${__dirname}/data/valacdos.json`, JSON.stringify(valacdos, null, 2)); | ||
writeFileSync( | ||
`${__dirname}/data/valacdos.json`, | ||
JSON.stringify(valacdos, null, 2) | ||
); | ||
}; | ||
processSubstitutionMap(); | ||
processModulusWeights(); | ||
processModulusWeights(); |
@@ -28,4 +28,8 @@ import { ModulusWeight } from './interfaces'; | ||
let adjustedSortCode = sortCode; | ||
if (modulusWeightException === 5 && substitutionMap[sortCode as keyof typeof substitutionMap]) { | ||
adjustedSortCode = substitutionMap[sortCode as keyof typeof substitutionMap]; | ||
if ( | ||
modulusWeightException === 5 && | ||
substitutionMap[sortCode as keyof typeof substitutionMap] | ||
) { | ||
adjustedSortCode = | ||
substitutionMap[sortCode as keyof typeof substitutionMap]; | ||
} else if (modulusWeightException === 8) { | ||
@@ -32,0 +36,0 @@ adjustedSortCode = '090126'; |
157
src/index.ts
@@ -11,85 +11,84 @@ import { ModulusWeight } from './interfaces'; | ||
export default class ModulusChecker { | ||
modulusCheck = ( | ||
modulusWeight: ModulusWeight, | ||
sortCode: string, | ||
accountNumber: string | ||
): boolean => { | ||
// by default, the account details are the sort code followed by the account number | ||
// there are exceptions to this rule, which are handled in the applyAccountDetailExceptionRules function | ||
const accountDetails = applyAccountDetailExceptionRules( | ||
sortCode, | ||
accountNumber, | ||
modulusWeight.exception | ||
); | ||
const modulusCalculation = ( | ||
modulusWeight: ModulusWeight, | ||
sortCode: string, | ||
accountNumber: string | ||
): boolean => { | ||
// by default, the account details are the sort code followed by the account number | ||
const accountDetails = applyAccountDetailExceptionRules( | ||
sortCode, | ||
accountNumber, | ||
modulusWeight.exception | ||
); | ||
// the default behaviour is to multiply and sum account details by the weight values and then carry out a modulus check | ||
// there are adjustments, where modulus check is skipped, or weights and account details are modified | ||
const weightValues = applyWeightValueExceptionRules( | ||
modulusWeight, | ||
accountDetails | ||
); | ||
const { modifiedAccountDetails, overwriteResult } = | ||
applyOverwriteExceptionRules(modulusWeight, accountDetails); | ||
if (overwriteResult !== null) return overwriteResult; | ||
// apply weight and exception rules | ||
const weightValues = applyWeightValueExceptionRules( | ||
modulusWeight, | ||
accountDetails | ||
); | ||
const { modifiedAccountDetails, overwriteResult } = | ||
applyOverwriteExceptionRules(modulusWeight, accountDetails); | ||
// multiply each digit of the account details by the corresponding weight value | ||
const multiplicationResultArray = modifiedAccountDetails | ||
if (overwriteResult !== null) return overwriteResult; | ||
// multiply each digit of the account details by the corresponding weight value | ||
const multiplicationResultArray = modifiedAccountDetails | ||
.split('') | ||
.map((digit, index) => parseInt(digit, 10) * weightValues[index]); | ||
// calculate total based on the check type | ||
let total: number; | ||
if (modulusWeight.check_type == CheckType.DBLAL) { | ||
total = multiplicationResultArray | ||
.map((num) => num.toString()) | ||
.join('') | ||
.split('') | ||
.map((digit, index) => parseInt(digit, 10) * weightValues[index]); | ||
.reduce((acc, digit) => acc + parseInt(digit, 10), 0); | ||
} else { | ||
total = multiplicationResultArray.reduce((acc, curr) => acc + curr, 0); | ||
} | ||
// the total is calculated differently depending on DBLAL | ||
// in the case of DBLAL, the total is the sum of the digits of the multiplication result | ||
// e.g. 18 -> 1 + 8 = 9 rather than 18 | ||
let total: number; | ||
if (modulusWeight.check_type == CheckType.DBLAL) { | ||
total = multiplicationResultArray | ||
.map((num) => num.toString()) | ||
.join('') | ||
.split('') | ||
.reduce((acc, digit) => acc + parseInt(digit, 10), 0); | ||
} else { | ||
total = multiplicationResultArray.reduce((acc, curr) => acc + curr, 0); | ||
} | ||
// there are exceptions that are applied after the total has been calculated | ||
// these can either adjust the total, or require a non-standard modulus check | ||
const { adjustedTotal, overwriteResult2 } = applyPostTotalExceptionRules( | ||
modulusWeight.exception, | ||
total, | ||
accountDetails | ||
); | ||
if (overwriteResult2 !== null) return overwriteResult2; | ||
const checkTypeValue = | ||
modulusWeight.check_type === CheckType.MOD11 ? 11 : 10; | ||
return adjustedTotal % checkTypeValue === 0; | ||
}; | ||
// apply post-total exception rules | ||
const { adjustedTotal, overwriteResult2 } = applyPostTotalExceptionRules( | ||
modulusWeight.exception, | ||
total, | ||
accountDetails | ||
); | ||
if (overwriteResult2 !== null) return overwriteResult2; | ||
validate(sortCode: string, accountNumber: string): boolean { | ||
// sort code must be 6 digits, account number must be between 6 and 10 digits | ||
if ( | ||
accountNumber.length <= 6 || | ||
accountNumber.length >= 10 || | ||
sortCode.length !== 6 | ||
) | ||
return false; | ||
// sort code and account number must be numeric | ||
if (!/^\d+$/.test(sortCode + accountNumber)) return false; | ||
// find the modulus weight that matches the sort code | ||
const matchingModulusWeights = modulusWeightsArray.filter( | ||
(weight) => | ||
weight.start && | ||
weight.end && | ||
parseInt(sortCode, 10) >= weight.start && | ||
parseInt(sortCode, 10) <= weight.end | ||
); | ||
// if there are no matching modulus weights, the sort code is not recognised | ||
// return true, since Vocalink data doesn't seem to have 100% coverage | ||
if (!matchingModulusWeights.length) return true; | ||
// if any of the matching modulus weights pass the modulus, the account number is valid | ||
// note, this is slightly conservative, and might return true for some invalid account numbers | ||
// find the actual spec. quite confusing on these cases | ||
return matchingModulusWeights.some((weight) => | ||
this.modulusCheck(weight as ModulusWeight, sortCode, accountNumber) | ||
); | ||
} | ||
} | ||
const checkTypeValue = modulusWeight.check_type === CheckType.MOD11 ? 11 : 10; | ||
return adjustedTotal % checkTypeValue === 0; | ||
}; | ||
export const validateAccountDetails = ( | ||
sortCode: string, | ||
accountNumber: string | ||
): boolean => { | ||
// sort code must be 6 digits, account number must be between 6 and 10 digits | ||
if ( | ||
accountNumber.length <= 6 || | ||
accountNumber.length >= 10 || | ||
sortCode.length !== 6 | ||
) | ||
return false; | ||
// sort code and account number must be numeric | ||
if (!/^\d+$/.test(sortCode + accountNumber)) return false; | ||
// find the modulus weight that matches the sort code | ||
const matchingModulusWeights = modulusWeightsArray.filter( | ||
(weight) => | ||
weight.start && | ||
weight.end && | ||
parseInt(sortCode, 10) >= weight.start && | ||
parseInt(sortCode, 10) <= weight.end | ||
); | ||
// if no matching weights, assume the sort code is valid by default | ||
if (!matchingModulusWeights.length) return true; | ||
// check if any matching weight passes the modulus check | ||
return matchingModulusWeights.some((weight) => | ||
modulusCalculation(weight as ModulusWeight, sortCode, accountNumber) | ||
); | ||
}; |
@@ -1,48 +0,9 @@ | ||
import ModulusChecker from '../src'; | ||
import { ModulusWeight } from '../src/interfaces'; | ||
import { CheckType } from '../src/enums'; | ||
import {validateAccountDetails} from '../src'; | ||
describe('ModulusChecker', () => { | ||
let checker: ModulusChecker; | ||
beforeEach(() => { | ||
checker = new ModulusChecker(); | ||
}); | ||
describe('modulusCheck', () => { | ||
const exampleWeights: ModulusWeight = { | ||
start: 499272, | ||
end: 499273, | ||
check_type: CheckType.DBLAL, | ||
exception: null, | ||
weights: [2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1], | ||
}; | ||
test('should pass example 1 from Vocalink spec', () => { | ||
const result = checker.modulusCheck(exampleWeights, '499273', '12345678'); | ||
expect(result).toBe(true); | ||
}); | ||
test('should fail example 1 from Vocalink spec with 1 increment', () => { | ||
const result = checker.modulusCheck(exampleWeights, '499273', '1234569'); | ||
expect(result).toBe(false); | ||
}); | ||
test('should pass example 2 from Vocalink spec', () => { | ||
const exampleWeights: ModulusWeight = { | ||
start: 0, | ||
end: 1, | ||
check_type: CheckType.MOD11, | ||
exception: null, | ||
weights: [0, 0, 0, 0, 0, 0, 7, 5, 8, 3, 4, 6, 2, 1], | ||
}; | ||
const result = checker.modulusCheck(exampleWeights, '000000', '58177632'); | ||
expect(result).toBe(true); | ||
}); | ||
}); | ||
describe('isValid', () => { | ||
// Custom tests | ||
test('should return false for a length 7 sort code', () => { | ||
const isValid = checker.validate('1234567', '12345678'); | ||
const isValid = validateAccountDetails('1234567', '12345678'); | ||
expect(isValid).toBe(false); | ||
@@ -52,3 +13,3 @@ }); | ||
test('should return false for a length 11 account number', () => { | ||
const isValid = checker.validate('000000', '12345678910'); | ||
const isValid = validateAccountDetails('000000', '12345678910'); | ||
expect(isValid).toBe(false); | ||
@@ -58,3 +19,3 @@ }); | ||
test('should return false for a non-numeric sort code', () => { | ||
const isValid = checker.validate('12345a', '12345678'); | ||
const isValid = validateAccountDetails('12345a', '12345678'); | ||
expect(isValid).toBe(false); | ||
@@ -64,3 +25,3 @@ }); | ||
test('should return true for a sort code not on the spec range', () => { | ||
const isValid = checker.validate('000000', '12345678'); | ||
const isValid = validateAccountDetails('000000', '12345678'); | ||
expect(isValid).toBe(true); | ||
@@ -112,3 +73,3 @@ }); | ||
test(`Vocalink spec test ${index + 1}`, () => { | ||
const isValid = checker.validate(sortCode, accountNumber); | ||
const isValid = validateAccountDetails(sortCode, accountNumber); | ||
expect(isValid).toBe(expectedResult); | ||
@@ -115,0 +76,0 @@ }); |
Sorry, the diff of this file is too big to display
71
1229860
59202