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