extended aurora scenarios + payment operation amounts
This commit is contained in:
@@ -13,14 +13,38 @@ Aurora is a dev/test-only card payout gateway with the same gRPC contract as `mn
|
||||
- No outbound payout/tokenization HTTP calls are made.
|
||||
|
||||
## Built-in test cards
|
||||
- `2200001111111111`: approved instantly (`success`, code `00`)
|
||||
- `2200002222222222`: pending issuer review (`waiting`, code `P01`)
|
||||
- `2200003333333333`: insufficient funds (`failed`, code `51`)
|
||||
- `2200004444444444`: issuer unavailable retryable (`failed`, code `10101`)
|
||||
- `2200005555555555`: stolen card (`failed`, code `43`)
|
||||
- `2200006666666666`: do not honor (`failed`, code `05`)
|
||||
- `2200007777777777`: expired card (`failed`, code `54`)
|
||||
- any other PAN: default queued processing (`waiting`, code `P00`)
|
||||
- Every scenario has multiple generated PANs (8 per case), all Luhn-valid.
|
||||
- PANs are generated from scenario prefixes in [`scenario_simulator.go`](./internal/service/gateway/scenario_simulator.go) via `generatePANSeriesWithLuhn`.
|
||||
- Scenario prefixes:
|
||||
- `22000011`: approved instantly (`success`, code `00`)
|
||||
- `22000022`: pending issuer review (`waiting`, code `P01`)
|
||||
- `22000033`: insufficient funds (`failed`, code `51`)
|
||||
- `22000044`: issuer unavailable retryable (`failed`, code `10101`)
|
||||
- `22000055`: stolen card (`failed`, code `43`)
|
||||
- `22000066`: do not honor (`failed`, code `05`)
|
||||
- `22000077`: expired card (`failed`, code `54`)
|
||||
- `22000088`: provider timeout transport error
|
||||
- `22000098`: provider unreachable transport error
|
||||
- `22000097`: provider maintenance (`failed`, code `91`)
|
||||
- `22000096`: provider system malfunction (`failed`, code `96`)
|
||||
- Any other valid PAN defaults to queued processing (`waiting`, code `P00`).
|
||||
|
||||
### Auxiliary PAN Table (Generated, Luhn-valid)
|
||||
|
||||
| Scenario | Prefix | PANs |
|
||||
|---|---|---|
|
||||
| approved_instant | `22000011` | `2200001100000001`<br>`2200001100000019`<br>`2200001100000027`<br>`2200001100000035`<br>`2200001100000043`<br>`2200001100000050`<br>`2200001100000068`<br>`2200001100000076` |
|
||||
| pending_issuer_review | `22000022` | `2200002200000008`<br>`2200002200000016`<br>`2200002200000024`<br>`2200002200000032`<br>`2200002200000040`<br>`2200002200000057`<br>`2200002200000065`<br>`2200002200000073` |
|
||||
| insufficient_funds | `22000033` | `2200003300000005`<br>`2200003300000013`<br>`2200003300000021`<br>`2200003300000039`<br>`2200003300000047`<br>`2200003300000054`<br>`2200003300000062`<br>`2200003300000070` |
|
||||
| issuer_unavailable_retryable | `22000044` | `2200004400000002`<br>`2200004400000010`<br>`2200004400000028`<br>`2200004400000036`<br>`2200004400000044`<br>`2200004400000051`<br>`2200004400000069`<br>`2200004400000077` |
|
||||
| stolen_card | `22000055` | `2200005500000008`<br>`2200005500000016`<br>`2200005500000024`<br>`2200005500000032`<br>`2200005500000040`<br>`2200005500000057`<br>`2200005500000065`<br>`2200005500000073` |
|
||||
| do_not_honor | `22000066` | `2200006600000005`<br>`2200006600000013`<br>`2200006600000021`<br>`2200006600000039`<br>`2200006600000047`<br>`2200006600000054`<br>`2200006600000062`<br>`2200006600000070` |
|
||||
| expired_card | `22000077` | `2200007700000002`<br>`2200007700000010`<br>`2200007700000028`<br>`2200007700000036`<br>`2200007700000044`<br>`2200007700000051`<br>`2200007700000069`<br>`2200007700000077` |
|
||||
| provider_timeout_transport | `22000088` | `2200008800000009`<br>`2200008800000017`<br>`2200008800000025`<br>`2200008800000033`<br>`2200008800000041`<br>`2200008800000058`<br>`2200008800000066`<br>`2200008800000074` |
|
||||
| provider_unreachable_transport | `22000098` | `2200009800000007`<br>`2200009800000015`<br>`2200009800000023`<br>`2200009800000031`<br>`2200009800000049`<br>`2200009800000056`<br>`2200009800000064`<br>`2200009800000072` |
|
||||
| provider_maintenance | `22000097` | `2200009700000008`<br>`2200009700000016`<br>`2200009700000024`<br>`2200009700000032`<br>`2200009700000040`<br>`2200009700000057`<br>`2200009700000065`<br>`2200009700000073` |
|
||||
| provider_system_malfunction | `22000096` | `2200009600000009`<br>`2200009600000017`<br>`2200009600000025`<br>`2200009600000033`<br>`2200009600000041`<br>`2200009600000058`<br>`2200009600000066`<br>`2200009600000074` |
|
||||
| default_processing (example) | `22000999` | `2200099900000007` |
|
||||
|
||||
## Notes
|
||||
- PAN is masked in logs.
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestAuroraCardPayoutScenarios(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "approved_instant",
|
||||
pan: "2200001111111111",
|
||||
pan: scenarioPAN("approved_instant", 0),
|
||||
wantAccepted: true,
|
||||
wantStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS,
|
||||
wantErrorCode: "00",
|
||||
@@ -42,7 +42,7 @@ func TestAuroraCardPayoutScenarios(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "pending_issuer_review",
|
||||
pan: "2200002222222222",
|
||||
pan: scenarioPAN("pending_issuer_review", 0),
|
||||
wantAccepted: true,
|
||||
wantStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING,
|
||||
wantErrorCode: "P01",
|
||||
@@ -51,7 +51,7 @@ func TestAuroraCardPayoutScenarios(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "insufficient_funds",
|
||||
pan: "2200003333333333",
|
||||
pan: scenarioPAN("insufficient_funds", 0),
|
||||
wantAccepted: false,
|
||||
wantStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED,
|
||||
wantErrorCode: "51",
|
||||
@@ -60,7 +60,7 @@ func TestAuroraCardPayoutScenarios(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "unknown_card_default_queue",
|
||||
pan: "2200009999999999",
|
||||
pan: defaultScenarioPAN(),
|
||||
wantAccepted: true,
|
||||
wantStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING,
|
||||
wantErrorCode: "P00",
|
||||
@@ -69,7 +69,7 @@ func TestAuroraCardPayoutScenarios(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "provider_maintenance",
|
||||
pan: "2200009999999997",
|
||||
pan: scenarioPAN("provider_maintenance", 0),
|
||||
wantAccepted: false,
|
||||
wantStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED,
|
||||
wantErrorCode: "91",
|
||||
@@ -78,7 +78,7 @@ func TestAuroraCardPayoutScenarios(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "provider_system_malfunction",
|
||||
pan: "2200009999999996",
|
||||
pan: scenarioPAN("provider_system_malfunction", 0),
|
||||
wantAccepted: false,
|
||||
wantStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED,
|
||||
wantErrorCode: "96",
|
||||
@@ -148,7 +148,7 @@ func TestAuroraTransportFailureScenarioEventuallyFails(t *testing.T) {
|
||||
req.OperationRef = "op-transport-timeout"
|
||||
req.ParentPaymentRef = "parent-transport-timeout"
|
||||
req.IdempotencyKey = "idem-transport-timeout"
|
||||
req.CardPan = "2200008888888888"
|
||||
req.CardPan = scenarioPAN("provider_timeout_transport", 0)
|
||||
|
||||
resp, err := processor.Submit(context.Background(), req)
|
||||
if err != nil {
|
||||
@@ -201,7 +201,7 @@ func TestAuroraRetryableScenarioEventuallyFails(t *testing.T) {
|
||||
req.OperationRef = "op-retryable-issuer-unavailable"
|
||||
req.ParentPaymentRef = "parent-retryable-issuer-unavailable"
|
||||
req.IdempotencyKey = "idem-retryable-issuer-unavailable"
|
||||
req.CardPan = "2200004444444444"
|
||||
req.CardPan = scenarioPAN("issuer_unavailable_retryable", 0)
|
||||
|
||||
resp, err := processor.Submit(context.Background(), req)
|
||||
if err != nil {
|
||||
@@ -248,7 +248,7 @@ func TestAuroraTokenPayoutUsesTokenizedPANScenario(t *testing.T) {
|
||||
|
||||
tokenizeReq := validCardTokenizeRequest()
|
||||
tokenizeReq.RequestId = "tok-req-insufficient"
|
||||
tokenizeReq.CardPan = "2200003333333333"
|
||||
tokenizeReq.CardPan = scenarioPAN("insufficient_funds", 0)
|
||||
|
||||
tokenizeResp, err := processor.Tokenize(context.Background(), tokenizeReq)
|
||||
if err != nil {
|
||||
|
||||
149
api/gateway/aurora/internal/service/gateway/card_pan.go
Normal file
149
api/gateway/aurora/internal/service/gateway/card_pan.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
const (
|
||||
minCardPANLength = 12
|
||||
maxCardPANLength = 19
|
||||
)
|
||||
|
||||
func normalizeCardNumber(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.Grow(len(value))
|
||||
for _, r := range value {
|
||||
if r >= '0' && r <= '9' {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func validateCardPAN(pan string, field string) error {
|
||||
normalized, err := normalizedPANForValidation(pan, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isValidPANLuhn(normalized) {
|
||||
return merrors.InvalidArgument("card_pan checksum is invalid", field)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizedPANForValidation(pan string, field string) (string, error) {
|
||||
field = strings.TrimSpace(field)
|
||||
if field == "" {
|
||||
field = "card_pan"
|
||||
}
|
||||
|
||||
clean := strings.TrimSpace(pan)
|
||||
if clean == "" {
|
||||
return "", merrors.InvalidArgument("card_pan is required", field)
|
||||
}
|
||||
|
||||
compact := strings.NewReplacer(" ", "", "-", "").Replace(clean)
|
||||
if compact == "" {
|
||||
return "", merrors.InvalidArgument("card_pan must contain only digits (spaces/hyphens allowed)", field)
|
||||
}
|
||||
|
||||
normalized := normalizeCardNumber(compact)
|
||||
if normalized != compact {
|
||||
return "", merrors.InvalidArgument("card_pan must contain only digits (spaces/hyphens allowed)", field)
|
||||
}
|
||||
if len(normalized) < minCardPANLength || len(normalized) > maxCardPANLength {
|
||||
return "", merrors.InvalidArgument(
|
||||
fmt.Sprintf("card_pan length must be between %d and %d", minCardPANLength, maxCardPANLength),
|
||||
field,
|
||||
)
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func isValidPANLuhn(number string) bool {
|
||||
if len(number) < minCardPANLength || len(number) > maxCardPANLength {
|
||||
return false
|
||||
}
|
||||
|
||||
sum := 0
|
||||
double := false
|
||||
for i := len(number) - 1; i >= 0; i-- {
|
||||
d := int(number[i] - '0')
|
||||
if d < 0 || d > 9 {
|
||||
return false
|
||||
}
|
||||
if double {
|
||||
d *= 2
|
||||
if d > 9 {
|
||||
d -= 9
|
||||
}
|
||||
}
|
||||
sum += d
|
||||
double = !double
|
||||
}
|
||||
return sum%10 == 0
|
||||
}
|
||||
|
||||
func generatePANWithLuhn(prefix string, sequence uint64, panLength int) (string, error) {
|
||||
prefix = normalizeCardNumber(prefix)
|
||||
if prefix == "" {
|
||||
return "", merrors.InvalidArgument("prefix is required", "prefix")
|
||||
}
|
||||
if panLength < minCardPANLength || panLength > maxCardPANLength {
|
||||
return "", merrors.InvalidArgument(
|
||||
fmt.Sprintf("pan_length must be between %d and %d", minCardPANLength, maxCardPANLength),
|
||||
"pan_length",
|
||||
)
|
||||
}
|
||||
|
||||
bodyLen := panLength - 1
|
||||
if len(prefix) > bodyLen {
|
||||
return "", merrors.InvalidArgument("prefix is too long for selected pan_length", "prefix")
|
||||
}
|
||||
|
||||
seqWidth := bodyLen - len(prefix)
|
||||
seqMod := pow10(seqWidth)
|
||||
body := prefix + fmt.Sprintf("%0*d", seqWidth, sequence%seqMod)
|
||||
|
||||
for checksum := 0; checksum <= 9; checksum++ {
|
||||
candidate := body + strconv.Itoa(checksum)
|
||||
if isValidPANLuhn(candidate) {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return "", merrors.Internal("failed to generate PAN checksum digit")
|
||||
}
|
||||
|
||||
func generatePANSeriesWithLuhn(prefix string, count int, panLength int) ([]string, error) {
|
||||
if count <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
out := make([]string, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
pan, err := generatePANWithLuhn(prefix, uint64(i), panLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, pan)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func pow10(n int) uint64 {
|
||||
if n <= 0 {
|
||||
return 1
|
||||
}
|
||||
p := uint64(1)
|
||||
for i := 0; i < n; i++ {
|
||||
p *= 10
|
||||
}
|
||||
return p
|
||||
}
|
||||
36
api/gateway/aurora/internal/service/gateway/card_pan_test.go
Normal file
36
api/gateway/aurora/internal/service/gateway/card_pan_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package gateway
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateCardPAN_AllowsFormattedInput(t *testing.T) {
|
||||
if err := validateCardPAN("4111 1111-1111 1111", "card_pan"); err != nil {
|
||||
t.Fatalf("expected formatted PAN to pass validation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCardPAN_RejectsInvalidChecksum(t *testing.T) {
|
||||
if err := validateCardPAN("4111111111111112", "card_pan"); err == nil {
|
||||
t.Fatal("expected invalid checksum to fail validation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePANSeriesWithLuhn_ProducesValidUniquePANs(t *testing.T) {
|
||||
pans, err := generatePANSeriesWithLuhn("22000033", 6, 16)
|
||||
if err != nil {
|
||||
t.Fatalf("generatePANSeriesWithLuhn returned error: %v", err)
|
||||
}
|
||||
if len(pans) != 6 {
|
||||
t.Fatalf("unexpected PAN count: got=%d want=6", len(pans))
|
||||
}
|
||||
|
||||
seen := map[string]struct{}{}
|
||||
for _, pan := range pans {
|
||||
if !isValidPANLuhn(pan) {
|
||||
t.Fatalf("generated PAN is not Luhn-valid: %q", pan)
|
||||
}
|
||||
if _, ok := seen[pan]; ok {
|
||||
t.Fatalf("duplicate PAN generated: %q", pan)
|
||||
}
|
||||
seen[pan] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,9 @@ func validateCardPayoutRequest(req *mntxv1.CardPayoutRequest, cfg provider.Confi
|
||||
if pan == "" {
|
||||
return newPayoutError("missing_card_pan", merrors.InvalidArgument("card_pan is required", "card_pan"))
|
||||
}
|
||||
if err := validateCardPAN(pan, "card_pan"); err != nil {
|
||||
return newPayoutError("invalid_card_pan_crc", err)
|
||||
}
|
||||
if strings.TrimSpace(req.GetCardHolder()) == "" {
|
||||
return newPayoutError("missing_card_holder", merrors.InvalidArgument("card_holder is required", "card_holder"))
|
||||
}
|
||||
|
||||
@@ -75,6 +75,11 @@ func TestValidateCardPayoutRequest_Errors(t *testing.T) {
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.CardPan = "" },
|
||||
expected: "missing_card_pan",
|
||||
},
|
||||
{
|
||||
name: "invalid_card_pan_crc",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.CardPan = "4111111111111112" },
|
||||
expected: "invalid_card_pan_crc",
|
||||
},
|
||||
{
|
||||
name: "missing_card_holder",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.CardHolder = "" },
|
||||
|
||||
@@ -295,14 +295,14 @@ func TestCardPayoutProcessor_Submit_SameParentDifferentOperationsStoredSeparatel
|
||||
req1.OperationRef = op1
|
||||
req1.IdempotencyKey = "idem-1"
|
||||
req1.ParentPaymentRef = parentPaymentRef
|
||||
req1.CardPan = "2204310000002456"
|
||||
req1.CardPan = scenarioPAN("approved_instant", 1)
|
||||
|
||||
req2 := validCardPayoutRequest()
|
||||
req2.PayoutId = ""
|
||||
req2.OperationRef = op2
|
||||
req2.IdempotencyKey = "idem-2"
|
||||
req2.ParentPaymentRef = parentPaymentRef
|
||||
req2.CardPan = "2204320000009754"
|
||||
req2.CardPan = scenarioPAN("pending_issuer_review", 1)
|
||||
|
||||
if _, err := processor.Submit(context.Background(), req1); err != nil {
|
||||
t.Fatalf("first submit failed: %v", err)
|
||||
@@ -385,14 +385,14 @@ func TestCardPayoutProcessor_StrictMode_BlocksSecondOperationUntilFirstFinalCall
|
||||
req1.OperationRef = "op-strict-1"
|
||||
req1.ParentPaymentRef = "payment-strict-1"
|
||||
req1.IdempotencyKey = "idem-strict-1"
|
||||
req1.CardPan = "2204310000002456"
|
||||
req1.CardPan = scenarioPAN("approved_instant", 2)
|
||||
|
||||
req2 := validCardPayoutRequest()
|
||||
req2.PayoutId = ""
|
||||
req2.OperationRef = "op-strict-2"
|
||||
req2.ParentPaymentRef = "payment-strict-2"
|
||||
req2.IdempotencyKey = "idem-strict-2"
|
||||
req2.CardPan = "2204320000009754"
|
||||
req2.CardPan = scenarioPAN("pending_issuer_review", 2)
|
||||
|
||||
if _, err := processor.Submit(context.Background(), req1); err != nil {
|
||||
t.Fatalf("first submit failed: %v", err)
|
||||
|
||||
@@ -41,6 +41,9 @@ func validateCardTokenizeRequest(req *mntxv1.CardTokenizeRequest, cfg provider.C
|
||||
if card.pan == "" {
|
||||
return nil, newPayoutError("missing_card_pan", merrors.InvalidArgument("card_pan is required", "card.pan"))
|
||||
}
|
||||
if err := validateCardPAN(card.pan, "card.pan"); err != nil {
|
||||
return nil, newPayoutError("invalid_card_pan_crc", err)
|
||||
}
|
||||
if card.holder == "" {
|
||||
return nil, newPayoutError("missing_card_holder", merrors.InvalidArgument("card_holder is required", "card.holder"))
|
||||
}
|
||||
|
||||
@@ -65,6 +65,15 @@ func TestValidateCardTokenizeRequest_MissingCardPan(t *testing.T) {
|
||||
requireReason(t, err, "missing_card_pan")
|
||||
}
|
||||
|
||||
func TestValidateCardTokenizeRequest_InvalidCardPanCRC(t *testing.T) {
|
||||
cfg := testProviderConfig()
|
||||
req := validCardTokenizeRequest()
|
||||
req.CardPan = "4111111111111112"
|
||||
|
||||
_, err := validateCardTokenizeRequest(req, cfg)
|
||||
requireReason(t, err, "invalid_card_pan_crc")
|
||||
}
|
||||
|
||||
func TestValidateCardTokenizeRequest_AddressRequired(t *testing.T) {
|
||||
cfg := testProviderConfig()
|
||||
cfg.RequireCustomerAddress = true
|
||||
|
||||
@@ -28,12 +28,31 @@ type payoutSimulator struct {
|
||||
seq atomic.Uint64
|
||||
}
|
||||
|
||||
const (
|
||||
scenarioPANLength = 16
|
||||
scenarioPANVariants = 8
|
||||
)
|
||||
|
||||
var scenarioPANPrefixes = map[string]string{
|
||||
"approved_instant": "22000011",
|
||||
"pending_issuer_review": "22000022",
|
||||
"insufficient_funds": "22000033",
|
||||
"issuer_unavailable_retryable": "22000044",
|
||||
"stolen_card": "22000055",
|
||||
"do_not_honor": "22000066",
|
||||
"expired_card": "22000077",
|
||||
"provider_timeout_transport": "22000088",
|
||||
"provider_unreachable_transport": "22000098",
|
||||
"provider_maintenance": "22000097",
|
||||
"provider_system_malfunction": "22000096",
|
||||
}
|
||||
|
||||
func newPayoutSimulator() *payoutSimulator {
|
||||
return &payoutSimulator{
|
||||
scenarios: []simulatedCardScenario{
|
||||
{
|
||||
Name: "approved_instant",
|
||||
CardNumbers: []string{"2200001111111111"},
|
||||
CardNumbers: mustScenarioPANs("approved_instant"),
|
||||
Accepted: true,
|
||||
ProviderStatus: "success",
|
||||
ErrorCode: "00",
|
||||
@@ -41,7 +60,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "pending_issuer_review",
|
||||
CardNumbers: []string{"2200002222222222"},
|
||||
CardNumbers: mustScenarioPANs("pending_issuer_review"),
|
||||
Accepted: true,
|
||||
ProviderStatus: "processing",
|
||||
ErrorCode: "P01",
|
||||
@@ -49,7 +68,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "insufficient_funds",
|
||||
CardNumbers: []string{"2200003333333333"},
|
||||
CardNumbers: mustScenarioPANs("insufficient_funds"),
|
||||
CardLast4: []string{"3333"},
|
||||
Accepted: false,
|
||||
ErrorCode: "51",
|
||||
@@ -57,7 +76,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "issuer_unavailable_retryable",
|
||||
CardNumbers: []string{"2200004444444444"},
|
||||
CardNumbers: mustScenarioPANs("issuer_unavailable_retryable"),
|
||||
CardLast4: []string{"4444"},
|
||||
Accepted: false,
|
||||
ErrorCode: "10101",
|
||||
@@ -65,7 +84,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "stolen_card",
|
||||
CardNumbers: []string{"2200005555555555"},
|
||||
CardNumbers: mustScenarioPANs("stolen_card"),
|
||||
CardLast4: []string{"5555"},
|
||||
Accepted: false,
|
||||
ErrorCode: "43",
|
||||
@@ -73,7 +92,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "do_not_honor",
|
||||
CardNumbers: []string{"2200006666666666"},
|
||||
CardNumbers: mustScenarioPANs("do_not_honor"),
|
||||
CardLast4: []string{"6666"},
|
||||
Accepted: false,
|
||||
ErrorCode: "05",
|
||||
@@ -81,7 +100,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "expired_card",
|
||||
CardNumbers: []string{"2200007777777777"},
|
||||
CardNumbers: mustScenarioPANs("expired_card"),
|
||||
CardLast4: []string{"7777"},
|
||||
Accepted: false,
|
||||
ErrorCode: "54",
|
||||
@@ -89,19 +108,19 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "provider_timeout_transport",
|
||||
CardNumbers: []string{"2200008888888888"},
|
||||
CardNumbers: mustScenarioPANs("provider_timeout_transport"),
|
||||
CardLast4: []string{"8888"},
|
||||
DispatchError: "provider timeout while calling payout endpoint",
|
||||
},
|
||||
{
|
||||
Name: "provider_unreachable_transport",
|
||||
CardNumbers: []string{"2200009999999998"},
|
||||
CardNumbers: mustScenarioPANs("provider_unreachable_transport"),
|
||||
CardLast4: []string{"9998"},
|
||||
DispatchError: "provider host unreachable",
|
||||
},
|
||||
{
|
||||
Name: "provider_maintenance",
|
||||
CardNumbers: []string{"2200009999999997"},
|
||||
CardNumbers: mustScenarioPANs("provider_maintenance"),
|
||||
CardLast4: []string{"9997"},
|
||||
Accepted: false,
|
||||
ErrorCode: "91",
|
||||
@@ -109,7 +128,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "provider_system_malfunction",
|
||||
CardNumbers: []string{"2200009999999996"},
|
||||
CardNumbers: mustScenarioPANs("provider_system_malfunction"),
|
||||
CardLast4: []string{"9996"},
|
||||
Accepted: false,
|
||||
ErrorCode: "96",
|
||||
@@ -126,6 +145,45 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
}
|
||||
}
|
||||
|
||||
func mustScenarioPANs(name string) []string {
|
||||
pans := scenarioPANs(name)
|
||||
if len(pans) == 0 {
|
||||
panic("aurora simulator scenario pan generation failed for " + name)
|
||||
}
|
||||
return pans
|
||||
}
|
||||
|
||||
func scenarioPANs(name string) []string {
|
||||
prefix := strings.TrimSpace(scenarioPANPrefixes[name])
|
||||
if prefix == "" {
|
||||
return nil
|
||||
}
|
||||
pans, err := generatePANSeriesWithLuhn(prefix, scenarioPANVariants, scenarioPANLength)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return pans
|
||||
}
|
||||
|
||||
func scenarioPAN(name string, index int) string {
|
||||
pans := scenarioPANs(name)
|
||||
if len(pans) == 0 {
|
||||
return ""
|
||||
}
|
||||
if index < 0 {
|
||||
index = 0
|
||||
}
|
||||
return pans[index%len(pans)]
|
||||
}
|
||||
|
||||
func defaultScenarioPAN() string {
|
||||
pan, err := generatePANWithLuhn("22000999", 0, scenarioPANLength)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return pan
|
||||
}
|
||||
|
||||
func (s *payoutSimulator) resolveByPAN(pan string) simulatedCardScenario {
|
||||
return s.resolve(normalizeCardNumber(pan), "")
|
||||
}
|
||||
@@ -210,21 +268,6 @@ func scenarioMatchesLast4(scenario simulatedCardScenario, last4 string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeCardNumber(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.Grow(len(value))
|
||||
for _, r := range value {
|
||||
if r >= '0' && r <= '9' {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func normalizeExpiryYear(year uint32) string {
|
||||
if year == 0 {
|
||||
return ""
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
func TestPayoutSimulatorResolveByPAN_KnownCard(t *testing.T) {
|
||||
sim := newPayoutSimulator()
|
||||
|
||||
scenario := sim.resolveByPAN("2200003333333333")
|
||||
scenario := sim.resolveByPAN(scenarioPAN("insufficient_funds", 0))
|
||||
if scenario.Name != "insufficient_funds" {
|
||||
t.Fatalf("unexpected scenario: got=%q", scenario.Name)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func TestPayoutSimulatorResolveByPAN_KnownCard(t *testing.T) {
|
||||
func TestPayoutSimulatorResolveByPAN_Default(t *testing.T) {
|
||||
sim := newPayoutSimulator()
|
||||
|
||||
scenario := sim.resolveByPAN("2200009999999999")
|
||||
scenario := sim.resolveByPAN(defaultScenarioPAN())
|
||||
if scenario.Name != "default_processing" {
|
||||
t.Fatalf("unexpected default scenario: got=%q", scenario.Name)
|
||||
}
|
||||
@@ -31,6 +31,21 @@ func TestPayoutSimulatorResolveByPAN_Default(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayoutSimulatorScenarioPANs_AreLuhnValidAndExpanded(t *testing.T) {
|
||||
sim := newPayoutSimulator()
|
||||
|
||||
for _, scenario := range sim.scenarios {
|
||||
if got, want := len(scenario.CardNumbers), scenarioPANVariants; got != want {
|
||||
t.Fatalf("expected %d PANs for scenario %q, got=%d", want, scenario.Name, got)
|
||||
}
|
||||
for _, pan := range scenario.CardNumbers {
|
||||
if !isValidPANLuhn(pan) {
|
||||
t.Fatalf("scenario %q has invalid Luhn PAN %q", scenario.Name, pan)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyCardPayoutSendResult_AcceptedSuccessStatus(t *testing.T) {
|
||||
state := &model.CardPayout{}
|
||||
result := &provider.CardPayoutSendResult{
|
||||
|
||||
Reference in New Issue
Block a user