From 95c9a727798fc86316e0db11781351ce7a44f3c1 Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 31 Jan 2025 21:08:14 +0200 Subject: [PATCH] api --- cmd/api.go | 46 ++++++++++++++++ lib/validate.go | 62 ---------------------- lib/validate_test.go | 60 --------------------- validation/validate.go | 95 +++++++++++++++++++++++++++++++++ validation/validate_test.go | 102 ++++++++++++++++++++++++++++++++++++ 5 files changed, 243 insertions(+), 122 deletions(-) delete mode 100644 lib/validate.go delete mode 100644 lib/validate_test.go create mode 100644 validation/validate.go create mode 100644 validation/validate_test.go diff --git a/cmd/api.go b/cmd/api.go index e69de29..1a0574a 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -0,0 +1,46 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + + "git.tertyshy.dev/cards/validation" +) + +type ValidatePayload struct { + Pan string `json:"pan"` + Month string `json:"month"` + Year string `json:"year"` +} + +type ValidationResponse struct { + Status int `json:"status"` + Error string `json:"error"` +} + +func validate(w http.ResponseWriter, r *http.Request) { + var payload ValidatePayload + var response ValidationResponse + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&payload) + + if err != nil { + response = ValidationResponse{ + -1, + err.Error(), + } + } else { + result := validation.Validate(validation.Card{payload.Pan, payload.Month, payload.Year}) + response = ValidationResponse{result.Code, result.Error.Error()} + } + + // todo: correct response type and code + json.NewEncoder(w).Encode(response) +} + +func main() { + http.HandleFunc("POST /validate", validate) + + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/lib/validate.go b/lib/validate.go deleted file mode 100644 index f2dd883..0000000 --- a/lib/validate.go +++ /dev/null @@ -1,62 +0,0 @@ -package validate - -import ( - "slices" - "strconv" - "strings" -) - -type Card struct { - Pan string - Month string - Year string -} - -type ValidationResult struct { - Code int - Error error -} - -func Luhn(pan string) bool { - if pan == "" { - return false - } - - lastIndex := len(pan) - 1 - - asDigits := []int{} - - for _, char := range strings.Split(pan, "") { - digit, err := strconv.Atoi(char) - - if err != nil { - return false - } - - asDigits = append(asDigits, digit) - } - - sum := 0 - - // do we really need reversed array? - for i, digit := range slices.Backward(asDigits[:lastIndex]) { - - if i%2 == len(pan)%2 { - if digit > 4 { - sum += 2*digit - 9 - } else { - sum += 2 * digit - } - } else { - sum += digit - } - - } - - return asDigits[lastIndex] == (10-(sum%10))%10 -} - -func Validate(card Card) (bool, ValidationResult) { - - return true, ValidationResult{} -} diff --git a/lib/validate_test.go b/lib/validate_test.go deleted file mode 100644 index fa8aad6..0000000 --- a/lib/validate_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package validate - -import "testing" - -func TestLuhn(t *testing.T) { - tests := []struct { - name string - pan string - want bool - }{ - { - "Mastercard", - "5555555555554444", - true, - }, - { - "Visa", - "4111111111111111", - true, - }, - { - "example from wiki", - "17893729974", - true, - }, - { - "another example", - "79927398713", - true, - }, - { - "worong check digit", - "17893729975", - false, - }, - { - "off by one error", - "27893729974", - false, - }, - { - "empty", - "", - false, - }, - { - "not a digit", - "27893a29974", - false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := Luhn(tt.pan) - if got != tt.want { - t.Errorf("Luhn(%s) = %v, want %v", tt.name, got, tt.want) - } - }) - } -} diff --git a/validation/validate.go b/validation/validate.go new file mode 100644 index 0000000..acc431c --- /dev/null +++ b/validation/validate.go @@ -0,0 +1,95 @@ +package validation + +import ( + "errors" + "slices" + "strconv" + "strings" + "time" +) + +type Card struct { + Pan string + Month string + Year string +} + +type ValidationResult struct { + Code int + Error error +} + +var ErrValid = ValidationResult{0, errors.New("the card is valid")} +var ErrInvalidPAN = ValidationResult{1, errors.New("invalid PAN")} +var ErrInvalidYear = ValidationResult{2, errors.New("invalid year")} +var ErrInvalidMonth = ValidationResult{3, errors.New("invalid month")} +var ErrExpire = ValidationResult{4, errors.New("expired card")} + +func Luhn(pan string) bool { + if pan == "" { + return false + } + + lastIndex := len(pan) - 1 + + asDigits := []int{} + + for _, char := range strings.Split(pan, "") { + digit, err := strconv.Atoi(char) + + if err != nil { + return false + } + + asDigits = append(asDigits, digit) + } + + sum := 0 + + // do we really need reversed iteration? + for i, digit := range slices.Backward(asDigits[:lastIndex]) { + + if i%2 == len(pan)%2 { + if digit > 4 { + sum += 2*digit - 9 + } else { + sum += 2 * digit + } + } else { + sum += digit + } + + } + + return asDigits[lastIndex] == (10-(sum%10))%10 +} + +func Validate(card Card) ValidationResult { + + if !Luhn(card.Pan) { + return ErrInvalidPAN + } + + year, yearErr := strconv.Atoi(card.Year) + + if yearErr != nil || len(card.Year) != 2 { + return ErrInvalidYear + } + + month, monthErr := strconv.Atoi(card.Month) + + if monthErr != nil || len(card.Month) != 2 || month > 12 || month < 1 { + return ErrInvalidMonth + } + + now := time.Now() + + // not sure about time zones + cardExpire := time.Date(year+2000, time.Month(month), 1, 0, 0, 0, 0, time.UTC) + + if now.After(cardExpire) { + return ErrExpire + } + + return ErrValid +} diff --git a/validation/validate_test.go b/validation/validate_test.go new file mode 100644 index 0000000..f22c6c4 --- /dev/null +++ b/validation/validate_test.go @@ -0,0 +1,102 @@ +package validation + +import "testing" + +func TestLuhn(t *testing.T) { + tests := []struct { + name string + pan string + want bool + }{ + { + "Mastercard", + "5555555555554444", + true, + }, + { + "Visa", + "4111111111111111", + true, + }, + { + "example from wiki", + "17893729974", + true, + }, + { + "another example", + "79927398713", + true, + }, + { + "worong check digit", + "17893729975", + false, + }, + { + "off by one error", + "27893729974", + false, + }, + { + "empty", + "", + false, + }, + { + "not a digit", + "27893a29974", + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Luhn(tt.pan) + if got != tt.want { + t.Errorf("Luhn(%s) = %v, want %v", tt.name, got, tt.want) + } + }) + } +} + +func TestValidate(t *testing.T) { + tests := []struct { + name string + card Card + result ValidationResult + }{ + { + "simple valid card", + Card{"4111111111111111", "09", "30"}, + ErrValid, + }, + { + "invalid PAN", + Card{"4111111111111114", "10", "26"}, + ErrInvalidPAN, + }, + { + "invalid year", + Card{"4111111111111111", "10", "2"}, + ErrInvalidYear, + }, + { + "invalid month", + Card{"4111111111111111", "13", "27"}, + ErrInvalidMonth, + }, + { + "expired", + Card{"4111111111111111", "10", "20"}, + ErrExpire, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Validate(tt.card) + if got != tt.result { + t.Errorf("Validate() = %v, want %v", got, tt.result) + } + }) + } +}