From 26c9fd6ea9caf87b1805ef29bfd13ef64b6cdc78 Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Wed, 21 Jun 2023 12:36:54 -0700 Subject: [PATCH] 29-credit-card-number-checker (#81) # Describe Request Credit card checker added. Fixes #29 # Change Type New checker. --- README.md | 1 + checker.go | 1 + credit_card.go | 146 ++++++++++++++++++++++ credit_card_test.go | 243 ++++++++++++++++++++++++++++++++++++ doc/checkers/credit_card.md | 62 +++++++++ 5 files changed, 453 insertions(+) create mode 100644 credit_card.go create mode 100644 credit_card_test.go create mode 100644 doc/checkers/credit_card.md diff --git a/README.md b/README.md index cf9433c..930269f 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ This package currently provides the following checkers: - [alphanumeric](doc/checkers/alphanumeric.md) checks if the given string consists of only alphanumeric characters. - [ascii](doc/checkers/ascii.md) checks if the given string consists of only ASCII characters. - [cidr](doc/checkers/cidr.md) checker checks if the value is a valid CIDR notation IP address and prefix length. +- [credit-card](doc/checkers/credit_card.md) checks if the given value is a valid credit card number. - [digits](doc/checkers/digits.md) checks if the given string consists of only digit characters. - [email](doc/checkers/email.md) checks if the given string is an email address. - [fqdn](doc/checkers/fqdn.md) checks if the given string is a fully qualified domain name. diff --git a/checker.go b/checker.go index 99e009b..169bae0 100644 --- a/checker.go +++ b/checker.go @@ -33,6 +33,7 @@ const ResultValid Result = "VALID" var makers = map[string]MakeFunc{ CheckerAlphanumeric: makeAlphanumeric, CheckerASCII: makeASCII, + CheckerCreditCard: makeCreditCard, CheckerCidr: makeCidr, CheckerDigits: makeDigits, CheckerEmail: makeEmail, diff --git a/credit_card.go b/credit_card.go new file mode 100644 index 0000000..4b5f453 --- /dev/null +++ b/credit_card.go @@ -0,0 +1,146 @@ +package checker + +import ( + "reflect" + "regexp" + "strings" +) + +// CheckerCreditCard is the name of the checker. +const CheckerCreditCard = "credit-card" + +// ResultNotCreditCard indicates that the given value is not a valid credit card number. +const ResultNotCreditCard = "NOT_CREDIT_CARD" + +// amexExpression is the regexp for the AMEX cards. They start with 34 or 37, and has 15 digits. +var amexExpression = "(?:^(?:3[47])[0-9]{13}$)" +var amexPattern = regexp.MustCompile(amexExpression) + +// dinersExpression is the regexp for the Diners cards. They start with 305, 36, 38, and has 14 digits. +var dinersExpression = "(?:^3(?:(?:05[0-9]{11})|(?:[68][0-9]{12}))$)" +var dinersPattern = regexp.MustCompile(dinersExpression) + +// discoverExpression is the regexp for the Discover cards. They start with 6011 and has 16 digits. +var discoverExpression = "(?:^6011[0-9]{12}$)" +var discoverPattern = regexp.MustCompile(discoverExpression) + +// jcbExpression is the regexp for the JCB 15 cards. They start with 2131, 1800, and has 15 digits, or start with 35 and has 16 digits. +var jcbExpression = "(?:^(?:(?:2131)|(?:1800)|(?:35[0-9]{3}))[0-9]{11}$)" +var jcbPattern = regexp.MustCompile(jcbExpression) + +// masterCardExpression is the regexp for the MasterCard cards. They start with 51, 52, 53, 54, or 55, and has 15 digits. +var masterCardExpression = "(?:^5[12345][0-9]{14}$)" +var masterCardPattern = regexp.MustCompile(masterCardExpression) + +// unionPayExpression is the regexp for the UnionPay cards. They start either with 62 or 67, and has 16 digits, or they start with 81 and has 16 to 19 digits. +var unionPayExpression = "(?:(?:6[27][0-9]{14})|(?:81[0-9]{14,17})^$)" +var unionPayPattern = regexp.MustCompile(unionPayExpression) + +// visaExpression is the regexp for the Visa cards. They start with 4 and has 13 or 16 digits. +var visaExpression = "(?:^4[0-9]{12}(?:[0-9]{3})?$)" +var visaPattern = regexp.MustCompile(visaExpression) + +// anyCreditCardPattern is the regexp for any credit card. +var anyCreditCardPattern = regexp.MustCompile(strings.Join([]string{ + amexExpression, + dinersExpression, + discoverExpression, + jcbExpression, + masterCardExpression, + unionPayExpression, + visaExpression, +}, "|")) + +// creditCardPatterns is the mapping for credit card names to patterns. +var creditCardPatterns = map[string]*regexp.Regexp{ + "amex": amexPattern, + "diners": dinersPattern, + "discover": discoverPattern, + "jcb": jcbPattern, + "mastercard": masterCardPattern, + "unionpay": unionPayPattern, + "visa": visaPattern, +} + +// IsAnyCreditCard checks if the given value is a valid credit card number. +func IsAnyCreditCard(number string) Result { + return isCreditCard(number, anyCreditCardPattern) +} + +// IsAmexCreditCard checks if the given valie is a valid AMEX credit card. +func IsAmexCreditCard(number string) Result { + return isCreditCard(number, amexPattern) +} + +// IsDinersCreditCard checks if the given valie is a valid Diners credit card. +func IsDinersCreditCard(number string) Result { + return isCreditCard(number, dinersPattern) +} + +// IsDiscoveryCreditCard checks if the given valie is a valid Discovery credit card. +func IsDiscoveryCreditCard(number string) Result { + return isCreditCard(number, discoverPattern) +} + +// IsJcbCreditCard checks if the given valie is a valid JCB 15 credit card. +func IsJcbCreditCard(number string) Result { + return isCreditCard(number, jcbPattern) +} + +// IsMasterCardCreditCard checks if the given valie is a valid MasterCard credit card. +func IsMasterCardCreditCard(number string) Result { + return isCreditCard(number, masterCardPattern) +} + +// IsUnionPayCreditCard checks if the given valie is a valid UnionPay credit card. +func IsUnionPayCreditCard(number string) Result { + return isCreditCard(number, unionPayPattern) +} + +// IsVisaCreditCard checks if the given valie is a valid Visa credit card. +func IsVisaCreditCard(number string) Result { + return isCreditCard(number, visaPattern) +} + +// makeCreditCard makes a checker function for the credit card checker. +func makeCreditCard(config string) CheckFunc { + patterns := []*regexp.Regexp{} + + if config != "" { + for _, card := range strings.Split(config, ",") { + pattern, ok := creditCardPatterns[card] + if !ok { + panic("unknown credit card name") + } + + patterns = append(patterns, pattern) + } + } else { + patterns = append(patterns, anyCreditCardPattern) + } + + return func(value, _ reflect.Value) Result { + if value.Kind() != reflect.String { + panic("string expected") + } + + number := value.String() + + for _, pattern := range patterns { + if isCreditCard(number, pattern) == ResultValid { + return ResultValid + } + } + + return ResultNotCreditCard + } +} + +// isCreditCard checks if the given number based on the given credit card pattern and the Luhn algorithm check digit. +func isCreditCard(number string, pattern *regexp.Regexp) Result { + if !pattern.MatchString(number) { + return ResultNotCreditCard + } + + return IsLuhn(number) +} diff --git a/credit_card_test.go b/credit_card_test.go new file mode 100644 index 0000000..1a35c16 --- /dev/null +++ b/credit_card_test.go @@ -0,0 +1,243 @@ +package checker + +import ( + "strconv" + "testing" +) + +// Test numbers from https://stripe.com/docs/testing +var invalidCard = "1234123412341234" +var amexCard = "378282246310005" +var dinersCard = "36227206271667" +var discoverCard = "6011111111111117" +var jcbCard = "3530111333300000" +var masterCard = "5555555555554444" +var unionPayCard = "6200000000000005" +var visaCard = "4111111111111111" + +// changeToInvalidLuhn increments the luhn digit to make the number invalid. It assumes that the given number is valid. +func changeToInvalidLuhn(number string) string { + luhn, err := strconv.Atoi(number[len(number)-1:]) + if err != nil { + panic(err) + } + + luhn = (luhn + 1) % 10 + + return number[:len(number)-1] + strconv.Itoa(luhn) +} + +func TestIsAnyCreditCardValid(t *testing.T) { + if IsAnyCreditCard(amexCard) != ResultValid { + t.Fail() + } +} + +func TestIsAnyCreditCardInvalidPattern(t *testing.T) { + if IsAnyCreditCard(invalidCard) == ResultValid { + t.Fail() + } +} + +func TestIsAnyCreditCardInvalidLuhn(t *testing.T) { + if IsAnyCreditCard(changeToInvalidLuhn(amexCard)) == ResultValid { + t.Fail() + } +} + +func TestIsAmexCreditCardValid(t *testing.T) { + if IsAmexCreditCard(amexCard) != ResultValid { + t.Fail() + } +} + +func TestIsAmexCreditCardInvalidPattern(t *testing.T) { + if IsAmexCreditCard(invalidCard) == ResultValid { + t.Fail() + } +} + +func TestIsAmexCreditCardInvalidLuhn(t *testing.T) { + if IsAmexCreditCard(changeToInvalidLuhn(amexCard)) == ResultValid { + t.Fail() + } +} + +func TestIsDinersCreditCardValid(t *testing.T) { + if IsDinersCreditCard(dinersCard) != ResultValid { + t.Fail() + } +} + +func TestIsDinersCreditCardInvalidPattern(t *testing.T) { + if IsDinersCreditCard(invalidCard) == ResultValid { + t.Fail() + } +} + +func TestIsDinersCreditCardInvalidLuhn(t *testing.T) { + if IsDinersCreditCard(changeToInvalidLuhn(dinersCard)) == ResultValid { + t.Fail() + } +} + +func TestIsDiscoverCreditCardValid(t *testing.T) { + if IsDiscoveryCreditCard(discoverCard) != ResultValid { + t.Fail() + } +} + +func TestIsDiscoverCreditCardInvalidPattern(t *testing.T) { + if IsDiscoveryCreditCard(invalidCard) == ResultValid { + t.Fail() + } +} + +func TestIsDiscoverCreditCardInvalidLuhn(t *testing.T) { + if IsDiscoveryCreditCard(changeToInvalidLuhn(discoverCard)) == ResultValid { + t.Fail() + } +} + +func TestIsJcbCreditCardValid(t *testing.T) { + if IsJcbCreditCard(jcbCard) != ResultValid { + t.Fail() + } +} + +func TestIsJcbCreditCardInvalidPattern(t *testing.T) { + if IsJcbCreditCard(invalidCard) == ResultValid { + t.Fail() + } +} + +func TestIsJcbCreditCardInvalidLuhn(t *testing.T) { + if IsJcbCreditCard(changeToInvalidLuhn(jcbCard)) == ResultValid { + t.Fail() + } +} + +func TestIsMasterCardCreditCardValid(t *testing.T) { + if IsMasterCardCreditCard(masterCard) != ResultValid { + t.Fail() + } +} + +func TestIsMasterCardCreditCardInvalidPattern(t *testing.T) { + if IsMasterCardCreditCard(invalidCard) == ResultValid { + t.Fail() + } +} + +func TestIsMasterCardCreditCardInvalidLuhn(t *testing.T) { + if IsMasterCardCreditCard(changeToInvalidLuhn(masterCard)) == ResultValid { + t.Fail() + } +} + +func TestIsUnionPayCreditCardValid(t *testing.T) { + if IsUnionPayCreditCard(unionPayCard) != ResultValid { + t.Fail() + } +} + +func TestIsUnionPayCreditCardInvalidPattern(t *testing.T) { + if IsUnionPayCreditCard(invalidCard) == ResultValid { + t.Fail() + } +} + +func TestIsUnionPayCreditCardInvalidLuhn(t *testing.T) { + if IsUnionPayCreditCard(changeToInvalidLuhn(unionPayCard)) == ResultValid { + t.Fail() + } +} + +func TestIsVisaCreditCardValid(t *testing.T) { + if IsVisaCreditCard(visaCard) != ResultValid { + t.Fail() + } +} + +func TestIsVisaCreditCardInvalidPattern(t *testing.T) { + if IsVisaCreditCard(invalidCard) == ResultValid { + t.Fail() + } +} + +func TestIsVisaCreditCardInvalidLuhn(t *testing.T) { + if IsVisaCreditCard(changeToInvalidLuhn(visaCard)) == ResultValid { + t.Fail() + } +} + +func TestCheckCreditCardNonString(t *testing.T) { + defer FailIfNoPanic(t) + + type Order struct { + CreditCard int `checkers:"credit-card"` + } + + order := &Order{} + + Check(order) +} + +func TestCheckCreditCardValid(t *testing.T) { + type Order struct { + CreditCard string `checkers:"credit-card"` + } + + order := &Order{ + CreditCard: amexCard, + } + + _, valid := Check(order) + if !valid { + t.Fail() + } +} + +func TestCheckCreditCardInvalid(t *testing.T) { + type Order struct { + CreditCard string `checkers:"credit-card"` + } + + order := &Order{ + CreditCard: invalidCard, + } + + _, valid := Check(order) + if valid { + t.Fail() + } +} + +func TestCheckCreditCardMultipleUnknown(t *testing.T) { + defer FailIfNoPanic(t) + + type Order struct { + CreditCard string `checkers:"credit-card:amex,unknown"` + } + + order := &Order{ + CreditCard: amexCard, + } + + Check(order) +} + +func TestCheckCreditCardMultipleInvalid(t *testing.T) { + type Order struct { + CreditCard string `checkers:"credit-card:amex,visa"` + } + + order := &Order{ + CreditCard: discoverCard, + } + + _, valid := Check(order) + if valid { + t.Fail() + } +} diff --git a/doc/checkers/credit_card.md b/doc/checkers/credit_card.md new file mode 100644 index 0000000..b5f47cd --- /dev/null +++ b/doc/checkers/credit_card.md @@ -0,0 +1,62 @@ +# Credit Card Checker + +The ```credit-card``` checker checks if the given value is a valid credit card number. If the given value is not a valid credit card number, the checker will return the ```NOT_CREDIT_CARD``` result. + +Here is an example: + +```golang +type Order struct { + CreditCard string `checkers:"credit-card"` +} + +order := &Order{ + CreditCard: invalidCard, +} + +_, valid := Check(order) +if valid { + // Send the mistakes back to the user +} +``` + +The checker currently knows about AMEX, Diners, Discover, JCB, MasterCard, UnionPay, and VISA credit card numbers. + +If you would like to check for a subset of those credit cards, you can specify them through the checker config parameter. Here is an example: + +```golang +type Order struct { + CreditCard string `checkers:"credit-card:amex,visa"` +} + +order := &Order{ + CreditCard: "6011111111111117", +} + +_, valid := Check(order) +if valid { + // Send the mistakes back to the user +} +``` + +If you would like to verify a credit card that is not listed here, please use the [luhn](luhn.md) checker to use the Luhn Algorithm to verify the check digit. + +In your custom checkers, you can call the ```credit-card``` checker functions below to validate the user input. + +- IsAnyCreditCard: checks if the given value is a valid credit card number. +- IsAmexCreditCard: checks if the given value is a valid AMEX credit card number. +- IsDinersCreditCard: checks if the given value is a valid Diners credit card number. +- IsDiscoverCreditCard: checks if the given value is a valid Discover credit card number. +- IsJcbCreditCard: checks if the given value is a valid JCB credit card number. +- IsMasterCardCreditCard: checks if the given value is a valid MasterCard credit card number. +- IsUnionPayCreditCard: checks if the given value is a valid UnionPay credit card number. +- IsVisaCreditCard: checks if the given value is a valid VISA credit card number. + +Here is an example: + +```golang +result := IsAnyCreditCard("6011111111111117") + +if result != ResultValid { + // Send the mistakes back to the user +} +```