diff --git a/src/decorator/decorators.ts b/src/decorator/decorators.ts index d449e9301..4a2483781 100644 --- a/src/decorator/decorators.ts +++ b/src/decorator/decorators.ts @@ -116,6 +116,7 @@ export * from './string/IsTimeZone'; export * from './string/IsBase58'; export * from './string/is-tax-id'; export * from './string/is-iso4217-currency-code'; +export * from './string/IsLuhn' // ------------------------------------------------------------------------- // Type checkers diff --git a/src/decorator/string/IsLuhn.ts b/src/decorator/string/IsLuhn.ts new file mode 100644 index 000000000..253289c03 --- /dev/null +++ b/src/decorator/string/IsLuhn.ts @@ -0,0 +1,48 @@ +import { ValidationOptions } from '../ValidationOptions'; +import { buildMessage, ValidateBy } from '../common/ValidateBy'; + +export const IS_LUHN = 'isLuhn'; + +/** + * Verify the card number using the Luhn algorithm. + * The Luhn algorithm is a simple checksum formula used to validate a variety of identification numbers, + * such as credit card numbers. + */ +export function isLuhn(value: unknown): boolean { + if(typeof value != 'string') return false; + if(value.length == 0) return false; + let nCheck = 0; + if (/[0-9-\s]+/.test(value)) { + value = value.replace(/\D/g, ''); + + (value as string).split('').forEach((v, n) => { + let nDigit = parseInt(v, 10); + + if (!(((value as string).length + n) % 2) && (nDigit *= 2) > 9) { + nDigit -= 9; + } + + nCheck += nDigit; + }); + } + + + return (nCheck % 10) === 0; +} + +/** + * Verify the card number using the Luhn algorithm. + * */ +export function IsLuhn(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: IS_LUHN, + validator: { + validate: (value, args): boolean => isLuhn(value), + defaultMessage: buildMessage(eachPrefix => eachPrefix + '$property must be a valid card number', validationOptions), + }, + }, + validationOptions + ); +} + diff --git a/test/functional/validation-functions-and-decorators.spec.ts b/test/functional/validation-functions-and-decorators.spec.ts index 9f938616c..2e847daa5 100644 --- a/test/functional/validation-functions-and-decorators.spec.ts +++ b/test/functional/validation-functions-and-decorators.spec.ts @@ -193,6 +193,7 @@ import { isTaxId, IsTaxId, IsISO4217CurrencyCode, + IsLuhn, isLuhn, } from '../../src/decorator/decorators'; import { Validator } from '../../src/validation/Validator'; import { ValidatorOptions } from '../../src/validation/ValidatorOptions'; @@ -789,6 +790,39 @@ describe('IsString', () => { }); }); +describe("IsLuhn", ()=>{ + const validValues = ['4532015112830366', '6011514433546201', '371449635398431', '6219861907174873']; + const invalidValues = ['4532015112830367', '1234567890123456', null, undefined, '', 6219861907174873]; + + class MyClass { + @IsLuhn() + someProperty: string; + } + + it('should not fail if validator.validate said that its valid', () => { + return checkValidValues(new MyClass(), validValues); + }); + + + it('should fail if validator.validate said that its invalid', () => { + return checkInvalidValues(new MyClass(), invalidValues); + }); + + it('should not fail if method in validator said that its valid', () => { + validValues.forEach(value => expect(isLuhn(value)).toBeTruthy()); + }); + + it('should fail if method in validator said that its invalid', () => { + invalidValues.forEach(value => expect(isLuhn(value as any)).toBeFalsy()); + }); + + it('should return error object with proper data', () => { + const validationType = 'isLuhn'; + const message = 'someProperty must be a valid card number'; + return checkReturnedError(new MyClass(), invalidValues, validationType, message); + }); +}) + describe('IsDateString', () => { const validValues = [ '2017-06-06T17:04:42.081Z',