diff --git a/v2/credit_card.go b/v2/credit_card.go new file mode 100644 index 0000000..32c9230 --- /dev/null +++ b/v2/credit_card.go @@ -0,0 +1,156 @@ +// Copyright (c) 2023-2024 Onur Cinar. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// https://github.com/cinar/checker + +package v2 + +import ( + "reflect" + "regexp" + "strings" +) + +const ( + // nameCreditCard is the name of the credit card check. + nameCreditCard = "credit-card" +) + +var ( + // ErrNotCreditCard indicates that the given value is not a valid credit card number. + ErrNotCreditCard = NewCheckError("CreditCard") + + // amexExpression is the regexp for the AMEX cards. They start with 34 or 37, and has 15 digits. + amexExpression = "(?:^(?:3[47])[0-9]{13}$)" + amexPattern = regexp.MustCompile(amexExpression) + + // dinersExpression is the regexp for the Diners cards. They start with 305, 36, 38, and has 14 digits. + dinersExpression = "(?:^3(?:(?:05[0-9]{11})|(?:[68][0-9]{12}))$)" + dinersPattern = regexp.MustCompile(dinersExpression) + + // discoverExpression is the regexp for the Discover cards. They start with 6011 and has 16 digits. + discoverExpression = "(?:^6011[0-9]{12}$)" + 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. + jcbExpression = "(?:^(?:(?:2131)|(?:1800)|(?:35[0-9]{3}))[0-9]{11}$)" + 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. + masterCardExpression = "(?:^5[12345][0-9]{14}$)" + 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. + unionPayExpression = "(?:(?:6[27][0-9]{14})|(?:81[0-9]{14,17})^$)" + unionPayPattern = regexp.MustCompile(unionPayExpression) + + // visaExpression is the regexp for the Visa cards. They start with 4 and has 13 or 16 digits. + visaExpression = "(?:^4[0-9]{12}(?:[0-9]{3})?$)" + visaPattern = regexp.MustCompile(visaExpression) + + // anyCreditCardPattern is the regexp for any credit card. + anyCreditCardPattern = regexp.MustCompile(strings.Join([]string{ + amexExpression, + dinersExpression, + discoverExpression, + jcbExpression, + masterCardExpression, + unionPayExpression, + visaExpression, + }, "|")) + + // creditCardPatterns is the mapping for credit card names to patterns. + 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) (string, error) { + return isCreditCard(number, anyCreditCardPattern) +} + +// IsAmexCreditCard checks if the given valie is a valid AMEX credit card. +func IsAmexCreditCard(number string) (string, error) { + return isCreditCard(number, amexPattern) +} + +// IsDinersCreditCard checks if the given valie is a valid Diners credit card. +func IsDinersCreditCard(number string) (string, error) { + return isCreditCard(number, dinersPattern) +} + +// IsDiscoverCreditCard checks if the given valie is a valid Discover credit card. +func IsDiscoverCreditCard(number string) (string, error) { + return isCreditCard(number, discoverPattern) +} + +// IsJcbCreditCard checks if the given valie is a valid JCB 15 credit card. +func IsJcbCreditCard(number string) (string, error) { + return isCreditCard(number, jcbPattern) +} + +// IsMasterCardCreditCard checks if the given valie is a valid MasterCard credit card. +func IsMasterCardCreditCard(number string) (string, error) { + return isCreditCard(number, masterCardPattern) +} + +// IsUnionPayCreditCard checks if the given valie is a valid UnionPay credit card. +func IsUnionPayCreditCard(number string) (string, error) { + return isCreditCard(number, unionPayPattern) +} + +// IsVisaCreditCard checks if the given valie is a valid Visa credit card. +func IsVisaCreditCard(number string) (string, error) { + return isCreditCard(number, visaPattern) +} + +// makeCreditCard makes a checker function for the credit card checker. +func makeCreditCard(config string) CheckFunc[reflect.Value] { + 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) (reflect.Value, error) { + if value.Kind() != reflect.String { + panic("string expected") + } + + number := value.String() + + for _, pattern := range patterns { + _, err := isCreditCard(number, pattern) + if err == nil { + return value, nil + } + } + + return value, ErrNotCreditCard + } +} + +// 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) (string, error) { + if !pattern.MatchString(number) { + return number, ErrNotCreditCard + } + + return IsLUHN(number) +} diff --git a/v2/credit_card_test.go b/v2/credit_card_test.go new file mode 100644 index 0000000..efedc17 --- /dev/null +++ b/v2/credit_card_test.go @@ -0,0 +1,312 @@ +// Copyright (c) 2023-2024 Onur Cinar. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// https://github.com/cinar/checker + +package v2_test + +import ( + "strconv" + "testing" + + v2 "github.com/cinar/checker/v2" +) + +// 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 ExampleIsAnyCreditCard() { + _, err := v2.IsAnyCreditCard("6011111111111117") + + if err != nil { + // Send the errors back to the user + } +} + +func TestIsAnyCreditCardValid(t *testing.T) { + _, err := v2.IsAnyCreditCard(amexCard) + if err != nil { + t.Error(err) + } +} + +func TestIsAnyCreditCardInvalidPattern(t *testing.T) { + if _, err := v2.IsAnyCreditCard(invalidCard); err == nil { + t.Error("expected error for invalid card pattern") + } +} + +func TestIsAnyCreditCardInvalidLuhn(t *testing.T) { + if _, err := v2.IsAnyCreditCard(changeToInvalidLuhn(amexCard)); err == nil { + t.Error("expected error for invalid Luhn") + } +} + +func ExampleIsAmexCreditCard() { + _, err := v2.IsAmexCreditCard("378282246310005") + + if err != nil { + // Send the errors back to the user + } +} + +func TestIsAmexCreditCardValid(t *testing.T) { + if _, err := v2.IsAmexCreditCard(amexCard); err != nil { + t.Error(err) + } +} + +func TestIsAmexCreditCardInvalidPattern(t *testing.T) { + if _, err := v2.IsAmexCreditCard(invalidCard); err == nil { + t.Error("expected error for invalid card pattern") + } +} + +func TestIsAmexCreditCardInvalidLuhn(t *testing.T) { + if _, err := v2.IsAmexCreditCard(changeToInvalidLuhn(amexCard)); err == nil { + t.Error("expected error for invalid Luhn") + } +} + +func ExampleIsDinersCreditCard() { + _, err := v2.IsDinersCreditCard("36227206271667") + + if err != nil { + // Send the errors back to the user + } +} +func TestIsDinersCreditCardValid(t *testing.T) { + if _, err := v2.IsDinersCreditCard(dinersCard); err != nil { + t.Error(err) + } +} + +func TestIsDinersCreditCardInvalidPattern(t *testing.T) { + if _, err := v2.IsDinersCreditCard(invalidCard); err == nil { + t.Error("expected error for invalid card pattern") + } +} + +func TestIsDinersCreditCardInvalidLuhn(t *testing.T) { + if _, err := v2.IsDinersCreditCard(changeToInvalidLuhn(dinersCard)); err == nil { + t.Error("expected error for invalid Luhn") + } +} + +func ExampleIsDiscoverCreditCard() { + _, err := v2.IsDiscoverCreditCard("6011111111111117") + + if err != nil { + // Send the errors back to the user + } +} +func TestIsDiscoverCreditCardValid(t *testing.T) { + if _, err := v2.IsDiscoverCreditCard(discoverCard); err != nil { + t.Error(err) + } +} + +func TestIsDiscoverCreditCardInvalidPattern(t *testing.T) { + if _, err := v2.IsDiscoverCreditCard(invalidCard); err == nil { + t.Error("expected error for invalid card pattern") + } +} + +func TestIsDiscoverCreditCardInvalidLuhn(t *testing.T) { + if _, err := v2.IsDiscoverCreditCard(changeToInvalidLuhn(discoverCard)); err == nil { + t.Error("expected error for invalid Luhn") + } +} + +func ExampleIsJcbCreditCard() { + _, err := v2.IsJcbCreditCard("3530111333300000") + + if err != nil { + // Send the errors back to the user + } +} + +func TestIsJcbCreditCardValid(t *testing.T) { + if _, err := v2.IsJcbCreditCard(jcbCard); err != nil { + t.Error(err) + } +} + +func TestIsJcbCreditCardInvalidPattern(t *testing.T) { + if _, err := v2.IsJcbCreditCard(invalidCard); err == nil { + t.Error("expected error for invalid card pattern") + } +} + +func TestIsJcbCreditCardInvalidLuhn(t *testing.T) { + if _, err := v2.IsJcbCreditCard(changeToInvalidLuhn(jcbCard)); err == nil { + t.Error("expected error for invalid Luhn") + } +} + +func ExampleIsMasterCardCreditCard() { + _, err := v2.IsMasterCardCreditCard("5555555555554444") + + if err != nil { + // Send the errors back to the user + } +} + +func TestIsMasterCardCreditCardValid(t *testing.T) { + if _, err := v2.IsMasterCardCreditCard(masterCard); err != nil { + t.Error(err) + } +} + +func TestIsMasterCardCreditCardInvalidPattern(t *testing.T) { + if _, err := v2.IsMasterCardCreditCard(invalidCard); err == nil { + t.Error("expected error for invalid card pattern") + } +} + +func TestIsMasterCardCreditCardInvalidLuhn(t *testing.T) { + if _, err := v2.IsMasterCardCreditCard(changeToInvalidLuhn(masterCard)); err == nil { + t.Error("expected error for invalid Luhn") + } +} + +func ExampleIsUnionPayCreditCard() { + _, err := v2.IsUnionPayCreditCard("6200000000000005") + + if err != nil { + // Send the errors back to the user + } +} + +func TestIsUnionPayCreditCardValid(t *testing.T) { + if _, err := v2.IsUnionPayCreditCard(unionPayCard); err != nil { + t.Error(err) + } +} + +func TestIsUnionPayCreditCardInvalidPattern(t *testing.T) { + if _, err := v2.IsUnionPayCreditCard(invalidCard); err == nil { + t.Error("expected error for invalid card pattern") + } +} + +func TestIsUnionPayCreditCardInvalidLuhn(t *testing.T) { + if _, err := v2.IsUnionPayCreditCard(changeToInvalidLuhn(unionPayCard)); err == nil { + t.Error("expected error for invalid Luhn") + } +} + +func ExampleIsVisaCreditCard() { + _, err := v2.IsVisaCreditCard("4111111111111111") + + if err != nil { + // Send the errors back to the user + } +} +func TestIsVisaCreditCardValid(t *testing.T) { + if _, err := v2.IsVisaCreditCard(visaCard); err != nil { + t.Error(err) + } +} + +func TestIsVisaCreditCardInvalidPattern(t *testing.T) { + if _, err := v2.IsVisaCreditCard(invalidCard); err == nil { + t.Error("expected error for invalid card pattern") + } +} + +func TestIsVisaCreditCardInvalidLuhn(t *testing.T) { + if _, err := v2.IsVisaCreditCard(changeToInvalidLuhn(visaCard)); err == nil { + t.Error("expected error for invalid Luhn") + } +} + +func TestCheckCreditCardNonString(t *testing.T) { + defer FailIfNoPanic(t, "expected panic for non-string credit card") + + type Order struct { + CreditCard int `checkers:"credit-card"` + } + + order := &Order{} + + v2.CheckStruct(order) +} + +func TestCheckCreditCardValid(t *testing.T) { + type Order struct { + CreditCard string `checkers:"credit-card"` + } + + order := &Order{ + CreditCard: amexCard, + } + + _, valid := v2.CheckStruct(order) + if !valid { + t.Fail() + } +} + +func TestCheckCreditCardInvalid(t *testing.T) { + type Order struct { + CreditCard string `checkers:"credit-card"` + } + + order := &Order{ + CreditCard: invalidCard, + } + + _, valid := v2.CheckStruct(order) + if valid { + t.Fail() + } +} + +func TestCheckCreditCardMultipleUnknown(t *testing.T) { + defer FailIfNoPanic(t, "expected panic for unknown credit card") + + type Order struct { + CreditCard string `checkers:"credit-card:amex,unknown"` + } + + order := &Order{ + CreditCard: amexCard, + } + + v2.CheckStruct(order) +} + +func TestCheckCreditCardMultipleInvalid(t *testing.T) { + type Order struct { + CreditCard string `checkers:"credit-card:amex,visa"` + } + + order := &Order{ + CreditCard: discoverCard, + } + + _, valid := v2.CheckStruct(order) + if valid { + t.Fail() + } +} diff --git a/v2/isbn_test.go b/v2/isbn_test.go index 2f763cf..cb8c48c 100644 --- a/v2/isbn_test.go +++ b/v2/isbn_test.go @@ -13,7 +13,7 @@ import ( ) func ExampleIsISBN() { - _, err := v2.IsISBN("9783161484100") + _, err := v2.IsISBN("1430248270") if err != nil { fmt.Println(err) } @@ -27,7 +27,7 @@ func TestIsISBNInvalid(t *testing.T) { } func TestIsISBNValid(t *testing.T) { - _, err := v2.IsISBN("9783161484100") + _, err := v2.IsISBN("1430248270") if err != nil { t.Fatal(err) } diff --git a/v2/luhn_test.go b/v2/luhn_test.go index 2dae79d..7831ccc 100644 --- a/v2/luhn_test.go +++ b/v2/luhn_test.go @@ -13,7 +13,7 @@ import ( ) func ExampleIsLUHN() { - _, err := v2.IsLUHN("79927398713") + _, err := v2.IsLUHN("4012888888881881") if err != nil { fmt.Println(err) } @@ -27,7 +27,7 @@ func TestIsLUHNInvalid(t *testing.T) { } func TestIsLUHNValid(t *testing.T) { - _, err := v2.IsLUHN("79927398713") + _, err := v2.IsLUHN("4012888888881881") if err != nil { t.Fatal(err) } diff --git a/v2/maker.go b/v2/maker.go index a056507..b5db335 100644 --- a/v2/maker.go +++ b/v2/maker.go @@ -19,6 +19,7 @@ var makers = map[string]MakeCheckFunc{ nameAlphanumeric: makeAlphanumeric, nameASCII: makeASCII, nameCIDR: makeCIDR, + nameCreditCard: makeCreditCard, nameDigits: makeDigits, nameEmail: makeEmail, nameFQDN: makeFQDN,