uk-modulus-check
Version:
TypeScript class that validates UK bank account details using the modulus checking algorithm.
120 lines (103 loc) • 3.79 kB
text/typescript
import { ModulusWeight } from './interfaces';
import { CheckType } from './enums';
import {
applyAccountDetailExceptionRules,
applyWeightValueExceptionRules,
applyOverwriteExceptionRules,
applyPostTotalExceptionRules,
} from './ExceptionRules';
import modulusWeightsArray from './data/valacdos.json';
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
);
// apply weight and exception rules
const weightValues = applyWeightValueExceptionRules(
modulusWeight,
accountDetails
);
const { modifiedAccountDetails, overwriteResult } =
applyOverwriteExceptionRules(modulusWeight, accountDetails, sortCode);
if (overwriteResult !== null) return overwriteResult;
// Helper function to perform the modulus check
const performCheck = (detailsToCheck: string, weights: number[]): boolean => {
// multiply each digit of the account details by the corresponding weight value
const multiplicationResultArray = detailsToCheck
.split('')
.map((digit, index) => parseInt(digit, 10) * weights[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('')
.reduce((acc, digit) => acc + parseInt(digit, 10), 0);
} else {
total = multiplicationResultArray.reduce((acc, curr) => acc + curr, 0);
}
// apply post-total exception rules
const { adjustedTotal, overwriteResult2 } = applyPostTotalExceptionRules(
modulusWeight.exception,
total,
detailsToCheck
);
if (overwriteResult2 !== null) return overwriteResult2;
const checkTypeValue =
modulusWeight.check_type === CheckType.MOD11 ? 11 : 10;
return adjustedTotal % checkTypeValue === 0;
};
// First stage: try with modified account details (or original if no modification)
if (performCheck(modifiedAccountDetails, weightValues)) {
return true;
}
// Exception 14: two-stage check - if first check fails, try with position 6 modified to 7
if (modulusWeight.exception === 14) {
const fallbackDetails =
accountDetails.slice(0, 6) + '7' + accountDetails.slice(7);
if (performCheck(fallbackDetails, weightValues)) {
return true;
}
}
return false;
};
export function 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;
const hasException6 = matchingModulusWeights.some((w) => w.exception === 6);
if (hasException6) {
return matchingModulusWeights.every((weight) =>
modulusCalculation(weight as ModulusWeight, sortCode, accountNumber)
);
}
return matchingModulusWeights.some((weight) =>
modulusCalculation(weight as ModulusWeight, sortCode, accountNumber)
);
}