diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e3f1d1..9b370b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# unreleased + +- Add `cardholderName` verification method + # 8.0.0 _Breaking Changes_ diff --git a/README.md b/README.md index 1133ca1..4571c62 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,35 @@ A fake session where a user is entering a card number may look like: --- +#### `valid.cardholderName(value: string): object` + +The `cardholderName` validation essentially tests for a valid string greater than 0 characters in length that does not look like a card number. + +```javascript +{ + isPotentiallyValid: true, + isValid: true +} +``` + +If a cardholder name is comprised of only numbers, hyphens and spaces, the validator considers it to be too card-like to be valid, but may still be potentially valid if a non-numeric character is added. This is to prevent card number values from being sent along as the cardholder name but not make too many assumptions about a person's cardholder name. + +```javascript +{ + isPotentiallyValid: true, + isValid: false +} +``` + +If a cardholder name is longer than 255 characters, it is assumed to be invalid. + +```javascript +{ + isPotentiallyValid: false, + isValid: false +} +``` + #### `valid.expirationDate(value: string|object, maxElapsedYear: integer): object` The `maxElapsedYear` parameter determines how many years in the future a card's expiration date should be considered valid. It has a default value of 19, so cards with an expiration date 20 or more years in the future would not be considered valid. It can be overridden by passing in an `integer` as a second argument. diff --git a/src/__tests__/cardholder-name.ts b/src/__tests__/cardholder-name.ts new file mode 100644 index 0000000..d46b226 --- /dev/null +++ b/src/__tests__/cardholder-name.ts @@ -0,0 +1,73 @@ +import { cardholderName } from "../cardholder-name"; +import type { Verification } from "../types"; + +describe("cardholderName", () => { + describe.each([ + [ + "returns false for non-string types", + [ + [0, { isValid: false, isPotentiallyValid: false }], + [0, { isValid: false, isPotentiallyValid: false }], + [123, { isValid: false, isPotentiallyValid: false }], + [1234, { isValid: false, isPotentiallyValid: false }], + [12345, { isValid: false, isPotentiallyValid: false }], + [557016, { isValid: false, isPotentiallyValid: false }], + [-1234, { isValid: false, isPotentiallyValid: false }], + [-10, { isValid: false, isPotentiallyValid: false }], + [0 / 0, { isValid: false, isPotentiallyValid: false }], + [Infinity, { isValid: false, isPotentiallyValid: false }], + [null, { isValid: false, isPotentiallyValid: false }], + [[], { isValid: false, isPotentiallyValid: false }], + [{}, { isValid: false, isPotentiallyValid: false }], + ], + ], + + [ + "returns false strings that are longer than 255 characters", + [ + [ + "this name is 256 chracters aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + { isValid: false, isPotentiallyValid: false }, + ], + ], + ], + + [ + "accepts valid cardholder names", + [ + ["name", { isValid: true, isPotentiallyValid: true }], + ["given sur", { isValid: true, isPotentiallyValid: true }], + [ + "this name is 255 chracters aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + { isValid: true, isPotentiallyValid: true }, + ], + ["name with many spaces", { isValid: true, isPotentiallyValid: true }], + [ + "name with number in it 01234", + { isValid: true, isPotentiallyValid: true }, + ], + ], + ], + + [ + "returns isPotentiallyValid for shorter-than-1 strings", + [["", { isValid: false, isPotentiallyValid: true }]], + ], + + [ + "returns isPotentiallyValid for strings with only numbers, spaces, and hyphens", + [ + ["4111", { isValid: false, isPotentiallyValid: true }], + ["4111 1111", { isValid: false, isPotentiallyValid: true }], + ["4111-1111", { isValid: false, isPotentiallyValid: true }], + ], + ], + ] as Array<[string, Array<[string, Verification]>]>)( + "%s", + (description, tests) => { + it.each(tests)("parses %s to be %p", (parseMe, meta) => { + expect(cardholderName(parseMe)).toEqual(meta); + }); + } + ); +}); diff --git a/src/cardholder-name.ts b/src/cardholder-name.ts new file mode 100644 index 0000000..219f765 --- /dev/null +++ b/src/cardholder-name.ts @@ -0,0 +1,31 @@ +import type { Verification } from "./types"; + +const CARD_NUMBER_REGEX = /^[\d\s-]*$/; +const MAX_LENGTH = 255; + +function verification( + isValid: boolean, + isPotentiallyValid: boolean +): Verification { + return { isValid, isPotentiallyValid }; +} + +export function cardholderName(value: string | unknown): Verification { + if (typeof value !== "string") { + return verification(false, false); + } + + if (value.length === 0) { + return verification(false, true); + } + + if (value.length > MAX_LENGTH) { + return verification(false, false); + } + + if (CARD_NUMBER_REGEX.test(value)) { + return verification(false, true); + } + + return verification(true, true); +} diff --git a/src/index.ts b/src/index.ts index 7d6e707..1361a1f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import * as creditCardType from "credit-card-type"; +import { cardholderName } from "./cardholder-name"; import { cardNumber as number } from "./card-number"; import { expirationDate } from "./expiration-date"; import { expirationMonth } from "./expiration-month"; @@ -9,6 +10,7 @@ import { postalCode } from "./postal-code"; const cardValidator = { creditCardType, + cardholderName, number, expirationDate, expirationMonth,