api
This commit is contained in:
parent
06198ced9a
commit
95c9a72779
5 changed files with 243 additions and 122 deletions
46
cmd/api.go
46
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))
|
||||||
|
}
|
|
@ -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{}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
95
validation/validate.go
Normal file
95
validation/validate.go
Normal file
|
@ -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
|
||||||
|
}
|
102
validation/validate_test.go
Normal file
102
validation/validate_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue