Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor #6

Merged
merged 3 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
test:
go test -count=2 .

coverage:
go test -coverprofile=coverage.out .
go tool cover -html=coverage.out
69 changes: 28 additions & 41 deletions creditcard.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// Package creditcard provides methods for validating credit cards
package creditcard

import (
"errors"
"strconv"
"time"
)

// Card holds generic information about the credit card
Expand Down Expand Up @@ -99,9 +97,10 @@ func (c *Card) Validate(allowTestNumbers ...bool) error {
func (c *Card) ValidateExpiration() error {
var year, month int
var err error
timeNow := timeNowCaller()

if len(c.Year) < 3 {
year, err = strconv.Atoi(strconv.Itoa(time.Now().UTC().Year())[:2] + c.Year)
year, err = strconv.Atoi(strconv.Itoa(timeNow.UTC().Year())[:2] + c.Year)
if err != nil {
return errors.New("Invalid year")
}
Expand All @@ -121,11 +120,11 @@ func (c *Card) ValidateExpiration() error {
return errors.New("Invalid month")
}

if year < time.Now().UTC().Year() {
if year < timeNowCaller().UTC().Year() {
return errors.New("Credit card has expired")
}

if year == time.Now().UTC().Year() && month < int(time.Now().UTC().Month()) {
if year == timeNowCaller().UTC().Year() && month < int(timeNowCaller().UTC().Month()) {
return errors.New("Credit card has expired")
}

Expand Down Expand Up @@ -170,62 +169,46 @@ func (c *Card) MethodValidate() (Company, error) {
}

switch {
case ccDigits.At(2) == 34 || ccDigits.At(2) == 37:
case isAmex(ccDigits):
return Company{"amex", "American Express"}, nil
case ccDigits.At(4) == 5610 || (ccDigits.At(6) >= 560221 && ccDigits.At(6) <= 560225):
case isBankCard(ccDigits):
return Company{"bankcard", "Bankcard"}, nil
case ccDigits.At(6) == 604400 || ccDigits.At(6) == 627170 || ccDigits.At(6) == 603522 || ccDigits.At(6) == 589657 || (ccDigits.At(6) >= 604201 && ccDigits.At(6) <= 604219) || (ccDigits.At(6) >= 604300 && ccDigits.At(6) <= 604399):
case isCabal(ccDigits):
return Company{"cabal", "Cabal"}, nil
case ccDigits.At(2) == 62 || ccDigits.At(2) == 81:
case isUnionPay(ccDigits):
return Company{"china unionpay", "China UnionPay"}, nil
case ccDigits.At(3) >= 300 && ccDigits.At(3) <= 305 && ccLen == 15:
case isDinersClubCarteBlance(ccDigits, ccLen):
return Company{"diners club carte blanche", "Diners Club Carte Blanche"}, nil
case ccDigits.At(4) == 2014 || ccDigits.At(4) == 2149:
case isDinersClubEnroute(ccDigits):
return Company{"diners club enroute", "Diners Club enRoute"}, nil
case ((ccDigits.At(3) >= 300 && ccDigits.At(3) <= 305) || ccDigits.At(3) == 309 || ccDigits.At(2) == 36 || ccDigits.At(2) == 38 || ccDigits.At(2) == 39) && ccLen <= 14:
case isDinersClubInternational(ccDigits, ccLen):
return Company{"diners club international", "Diners Club International"}, nil
case ccDigits.At(4) == 6011 || (ccDigits.At(6) >= 622126 && ccDigits.At(6) <= 622925) || (ccDigits.At(3) >= 644 && ccDigits.At(3) <= 649) || ccDigits.At(2) == 65:
case isDiscover(ccDigits):
return Company{"discover", "Discover"}, nil
// Elo must be checked before interpayment
case ccDigits.At(4) == 4011 || ccDigits.At(6) == 431274 || ccDigits.At(6) == 438935 ||
ccDigits.At(6) == 451416 || ccDigits.At(6) == 457393 || ccDigits.At(4) == 4576 ||
ccDigits.At(6) == 457631 || ccDigits.At(6) == 457632 || ccDigits.At(6) == 504175 ||
ccDigits.At(6) == 627780 || ccDigits.At(6) == 636297 || ccDigits.At(6) == 636368 ||
ccDigits.At(6) == 636369 || (ccDigits.At(6) >= 506699 && ccDigits.At(6) <= 506778) ||
(ccDigits.At(6) >= 509000 && ccDigits.At(6) <= 509999) ||
(ccDigits.At(6) >= 650031 && ccDigits.At(6) <= 650051) ||
(ccDigits.At(6) >= 650035 && ccDigits.At(6) <= 650033) ||
(ccDigits.At(6) >= 650405 && ccDigits.At(6) <= 650439) ||
(ccDigits.At(6) >= 650485 && ccDigits.At(6) <= 650538) ||
(ccDigits.At(6) >= 650541 && ccDigits.At(6) <= 650598) ||
(ccDigits.At(6) >= 650700 && ccDigits.At(6) <= 650718) ||
(ccDigits.At(6) >= 650720 && ccDigits.At(6) <= 650727) ||
(ccDigits.At(6) >= 650901 && ccDigits.At(6) <= 650920) ||
(ccDigits.At(6) >= 651652 && ccDigits.At(6) <= 651679) ||
(ccDigits.At(6) >= 655000 && ccDigits.At(6) <= 655019) ||
(ccDigits.At(6) >= 655021 && ccDigits.At(6) <= 655021):
case isElo(ccDigits):
return Company{"elo", "Elo"}, nil
case matchesValue(ccDigits.At(6), []int{606282, 637095, 637568, 637599, 637609, 637612}):
case isHipercard(ccDigits):
return Company{"hipercard", "Hipercard"}, nil
case ccDigits.At(3) == 636 && ccLen >= 16 && ccLen <= 19:
case isInterpayment(ccDigits, ccLen):
return Company{"interpayment", "InterPayment"}, nil
case ccDigits.At(3) >= 637 && ccDigits.At(3) <= 639 && ccLen == 16:
case isInstapayment(ccDigits, ccLen):
return Company{"instapayment", "InstaPayment"}, nil
case ccDigits.At(4) >= 3528 && ccDigits.At(4) <= 3589:
case isJCB(ccDigits):
return Company{"jcb", "JCB"}, nil
case ccDigits.At(6) == 589562:
case isNaranja(ccDigits):
return Company{"naranja", "Naranja"}, nil
case ccDigits.At(4) == 5018 || ccDigits.At(4) == 5020 || ccDigits.At(4) == 5038 || ccDigits.At(4) == 5612 || ccDigits.At(4) == 5893 || ccDigits.At(4) == 6304 || ccDigits.At(4) == 6759 || ccDigits.At(4) == 6761 || ccDigits.At(4) == 6762 || ccDigits.At(4) == 6763 || c.Number[:3] == "0604" || ccDigits.At(4) == 6390:
case isMaestro(c, ccDigits):
return Company{"maestro", "Maestro"}, nil
case ccDigits.At(4) == 5019:
case isDankort(ccDigits):
return Company{"dankort", "Dankort"}, nil
case ccDigits.At(2) >= 51 && ccDigits.At(2) <= 55 || ccDigits.At(6) >= 222100 && ccDigits.At(6) <= 272099:
case isMasterCard(ccDigits):
return Company{"mastercard", "MasterCard"}, nil
case ccDigits.At(4) == 4026 || ccDigits.At(6) == 417500 || ccDigits.At(4) == 4405 || ccDigits.At(4) == 4508 || ccDigits.At(4) == 4844 || ccDigits.At(4) == 4913 || ccDigits.At(4) == 4917:
case isVisaElectron(ccDigits):
return Company{"visa electron", "Visa Electron"}, nil
case ccDigits.At(1) == 4:
case isVisa(ccDigits):
return Company{"visa", "Visa"}, nil
case ccDigits.At(2) == 50:
case isAura(ccDigits):
return Company{"aura", "Aura"}, nil
default:
return Company{"", ""}, errors.New("Unknown credit card method")
Expand Down Expand Up @@ -271,3 +254,7 @@ func matchesValue(number int, numbers []int) bool {
}
return false
}

func isInBetween(n, min, max int) bool {
return n >= min && n <= max
}
17 changes: 16 additions & 1 deletion creditcard_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package creditcard

import (
. "github.com/smartystreets/goconvey/convey"
"strconv"
"testing"
"time"

. "github.com/smartystreets/goconvey/convey"
)

func TestFourDigits(t *testing.T) {
Expand Down Expand Up @@ -466,3 +467,17 @@ func TestMethod(t *testing.T) {
})
})
}

func TestCard_ValidateExpiration(t *testing.T) {
Convey("should get an error if card year is current year and card month is before current month", t, func() {
defer resetMocks()
timeNowCaller = func() time.Time {
return time.Date(2001, 3, 1, 1, 1, 1, 1, time.UTC)
}
card := Card{Number: "4012888888881881", Cvv: "111", Month: "02", Year: "2001"}

err := card.ValidateExpiration()
So(err, ShouldNotBeNil)
So(err.Error(), ShouldEqual, "Credit card has expired")
})
}
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
module github.com/durango/go-credit-card

go 1.13
go 1.17

require github.com/smartystreets/goconvey v1.6.4

require (
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
Expand Down
10 changes: 10 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Package creditcard provides methods for validating credit cards
package creditcard

import "time"

var timeNowCaller = time.Now

func resetMocks() {
timeNowCaller = time.Now
}
109 changes: 109 additions & 0 deletions validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package creditcard

func isAmex(ccDigits digits) bool {
return matchesValue(ccDigits.At(2), []int{34, 37})
}

func isBankCard(ccDigits digits) bool {
return ccDigits.At(4) == 5610 || isInBetween(ccDigits.At(6), 560221, 560225)
}

func isCabal(ccDigits digits) bool {
atSix := ccDigits.At(6)

return matchesValue(atSix, []int{604400, 627170, 603522, 589657}) ||
isInBetween(atSix, 604201, 604219) ||
isInBetween(atSix, 604300, 604399)
}

func isUnionPay(ccDigits digits) bool {
return matchesValue(ccDigits.At(2), []int{62, 81})
}

func isDinersClubCarteBlance(ccDigits digits, ccLen int) bool {
return isInBetween(ccDigits.At(3), 300, 305) && ccLen == 15
}

func isDinersClubEnroute(ccDigits digits) bool {
return matchesValue(ccDigits.At(4), []int{2014, 2149})
}

func isDinersClubInternational(ccDigits digits, ccLen int) bool {
checkThree := isInBetween(ccDigits.At(3), 300, 305) || ccDigits.At(3) == 309
checkTwoo := matchesValue(ccDigits.At(2), []int{36, 38, 39})

return (checkThree || checkTwoo) && ccLen <= 14
}

func isDiscover(ccDigits digits) bool {
return ccDigits.At(4) == 6011 ||
isInBetween(ccDigits.At(6), 622126, 622925) ||
isInBetween(ccDigits.At(3), 644, 649) ||
ccDigits.At(2) == 65
}

func isElo(ccDigits digits) bool {
atFour := ccDigits.At(4)
atSix := ccDigits.At(6)

return matchesValue(atFour, []int{4011, 4576}) ||
matchesValue(atSix, []int{431274, 438935, 451416, 457393, 457631, 457632, 504175, 627780, 636297, 636368, 636369}) ||
isInBetween(atSix, 506699, 506778) ||
isInBetween(atSix, 509000, 509999) ||
isInBetween(atSix, 650031, 650051) ||
isInBetween(atSix, 650035, 650033) ||
isInBetween(atSix, 650405, 650439) ||
isInBetween(atSix, 650485, 650538) ||
isInBetween(atSix, 650541, 650598) ||
isInBetween(atSix, 650700, 650718) ||
isInBetween(atSix, 650720, 650727) ||
isInBetween(atSix, 650901, 650920) ||
isInBetween(atSix, 651652, 651679) ||
isInBetween(atSix, 655000, 655019) ||
isInBetween(atSix, 655021, 655021)
}

func isHipercard(ccDigits digits) bool {
return matchesValue(ccDigits.At(6), []int{606282, 637095, 637568, 637599, 637609, 637612})
}

func isInterpayment(ccDigits digits, ccLen int) bool {
return ccDigits.At(3) == 636 && isInBetween(ccLen, 16, 19)
}

func isInstapayment(ccDigits digits, ccLen int) bool {
return isInBetween(ccDigits.At(3), 637, 639) && ccLen == 16
}

func isJCB(ccDigits digits) bool {
return isInBetween(ccDigits.At(4), 3528, 3589)
}

func isNaranja(ccDigits digits) bool {
return ccDigits.At(6) == 589562
}

func isMaestro(c *Card, ccDigits digits) bool {
return matchesValue(ccDigits.At(4), []int{5018, 5020, 5038, 5612, 5893, 6304, 6759, 6761, 6762, 6763, 6390}) ||
c.Number[:3] == "0604"
}

func isDankort(ccDigits digits) bool {
return ccDigits.At(4) == 5019
}

func isMasterCard(ccDigits digits) bool {
return isInBetween(ccDigits.At(2), 51, 55) || isInBetween(ccDigits.At(6), 222100, 272099)
}

func isVisaElectron(ccDigits digits) bool {
return matchesValue(ccDigits.At(4), []int{4026, 4405, 4508, 4844, 4913, 4917}) || ccDigits.At(6) == 417500
}

func isVisa(ccDigits digits) bool {
return ccDigits.At(1) == 4
}

func isAura(ccDigits digits) bool {
return ccDigits.At(2) == 50
}