시리즈: Go
go
132 줄
· 업데이트 2026-04-18
payment.go
Go/payment-gateway-challenge/internal/validation/payment.go
// Package validation implements the Checkout.com assessment validation rules
// for inbound payment requests. Any failure produces a human-readable reason
// suitable for a "Rejected" response without contacting the acquiring bank.
package validation
import (
"fmt"
"regexp"
"strings"
"time"
)
// ProcessPaymentRequest is the public JSON contract for POST /api/payments.
// `example` tags are consumed by Swag when generating OpenAPI / Swagger UI.
type ProcessPaymentRequest struct {
CardNumber string `json:"card_number" example:"2222405343248877"`
ExpiryMonth int `json:"expiry_month" example:"4"`
ExpiryYear int `json:"expiry_year" example:"2028"`
Currency string `json:"currency" example:"GBP"`
Amount int64 `json:"amount" example:"100"`
CVV string `json:"cvv" example:"123"`
}
var digitsOnly = regexp.MustCompile(`^[0-9]+$`)
// ValidateProcessRequest applies every rule from the recruitment README table.
// allowedCurrencies must already be normalised uppercase (max 3 codes).
func ValidateProcessRequest(r ProcessPaymentRequest, allowedCurrencies []string, now time.Time) []string {
var errs []string
add := func(msg string) { errs = append(errs, msg) }
pan := strings.TrimSpace(r.CardNumber)
if pan == "" {
add("card_number is required")
} else {
if len(pan) < 14 || len(pan) > 19 {
add("card_number must be between 14 and 19 characters long")
}
if !digitsOnly.MatchString(pan) {
add("card_number must only contain numeric characters")
}
}
if r.ExpiryMonth < 1 || r.ExpiryMonth > 12 {
add("expiry_month must be between 1 and 12")
}
if r.ExpiryYear < 2000 || r.ExpiryYear > 2100 {
// Not explicitly in spec — guards obviously bogus years before date logic.
add("expiry_year is out of supported range")
} else if r.ExpiryMonth >= 1 && r.ExpiryMonth <= 12 {
// Cards remain valid through the end of the printed expiry month.
// (Industry-standard interpretation; avoids rejecting mid-month edge cases.)
expEnd := time.Date(r.ExpiryYear, time.Month(r.ExpiryMonth+1), 0, 23, 59, 59, 999999999, time.UTC)
if expEnd.Before(now.UTC()) {
add("expiry month and year combination must still be valid (card appears expired)")
}
}
cur := strings.TrimSpace(strings.ToUpper(r.Currency))
if cur == "" {
add("currency is required")
} else if len(cur) != 3 {
add("currency must be exactly 3 characters (ISO 4217)")
} else {
ok := false
for _, c := range allowedCurrencies {
if c == cur {
ok = true
break
}
}
if !ok {
add(fmt.Sprintf("currency must be one of: %s", strings.Join(allowedCurrencies, ", ")))
}
}
if r.Amount <= 0 {
add("amount must be a positive integer (minor currency units)")
}
cvv := strings.TrimSpace(r.CVV)
if cvv == "" {
add("cvv is required")
} else {
if len(cvv) < 3 || len(cvv) > 4 {
add("cvv must be 3-4 characters long")
}
if !digitsOnly.MatchString(cvv) {
add("cvv must only contain numeric characters")
}
}
return errs
}
// BankSimulatorBody is the JSON contract expected by the Mountebank simulator.
type BankSimulatorBody struct {
CardNumber string `json:"card_number"`
ExpiryDate string `json:"expiry_date"` // MM/YYYY
Currency string `json:"currency"`
Amount int64 `json:"amount"`
CVV string `json:"cvv"`
}
// ToBankRequest maps a validated merchant request to the simulator shape.
func ToBankRequest(r ProcessPaymentRequest) BankSimulatorBody {
return BankSimulatorBody{
CardNumber: strings.TrimSpace(r.CardNumber),
ExpiryDate: fmt.Sprintf("%02d/%04d", r.ExpiryMonth, r.ExpiryYear),
Currency: strings.TrimSpace(strings.ToUpper(r.Currency)),
Amount: r.Amount,
CVV: strings.TrimSpace(r.CVV),
}
}
// LastFourDigits returns the final four digits of the PAN for safe storage/display.
func LastFourDigits(pan string) string {
pan = strings.TrimSpace(pan)
if len(pan) < 4 {
return ""
}
return pan[len(pan)-4:]
}
// MaskPAN renders a PCI-friendly mask: (len-4) asterisks + last four digits.
func MaskPAN(lastFour string, panLength int) string {
if panLength < 4 || len(lastFour) != 4 {
return "****"
}
return strings.Repeat("*", panLength-4) + lastFour
}
관련 글
Go
go
업데이트 2026-04-18
main.go
main.go — go source code from the Go learning materials (Go/Baasid/cmd/hashpw/main.go).
글 읽기 →
Go
go
업데이트 2026-04-18
main.go
main.go — go source code from the Go learning materials (Go/Baasid/cmd/server/main.go).
글 읽기 →
Go
go
업데이트 2026-04-18
docs.go
docs.go — go source code from the Go learning materials (Go/Baasid/docs/docs.go).
글 읽기 →
Go
go
업데이트 2026-04-18
jwt.go
jwt.go — go source code from the Go learning materials (Go/Baasid/internal/auth/jwt.go).
글 읽기 →
Go
go
업데이트 2026-04-18
password.go
password.go — go source code from the Go learning materials (Go/Baasid/internal/auth/password.go).
글 읽기 →
Go
go
업데이트 2026-04-18
password_test.go
password_test.go — go source code from the Go learning materials (Go/Baasid/internal/auth/password_test.go).
글 읽기 →