291 lines
7.4 KiB
Go
291 lines
7.4 KiB
Go
package gateway
|
|
|
|
import (
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type payoutFailureAction int
|
|
|
|
const (
|
|
payoutFailureActionFail payoutFailureAction = iota + 1
|
|
payoutFailureActionRetry
|
|
)
|
|
|
|
type payoutRetryStrategy int
|
|
|
|
const (
|
|
payoutRetryStrategyImmediate payoutRetryStrategy = iota + 1
|
|
payoutRetryStrategyDelayed
|
|
payoutRetryStrategyStatusRefresh
|
|
)
|
|
|
|
type payoutFailureDecision struct {
|
|
Action payoutFailureAction
|
|
Strategy payoutRetryStrategy
|
|
Reason string
|
|
}
|
|
|
|
type payoutFailurePolicy struct {
|
|
providerCodeStrategies map[string]payoutRetryStrategy
|
|
documentedProviderCodes map[string]struct{}
|
|
}
|
|
|
|
type retryCodeBucket struct {
|
|
strategy payoutRetryStrategy
|
|
retryable bool
|
|
codes []string
|
|
}
|
|
|
|
var providerRetryOnlyCodeBuckets = []retryCodeBucket{
|
|
// GTX "repeat request now / temporary issue" style codes.
|
|
{
|
|
strategy: payoutRetryStrategyImmediate,
|
|
retryable: true,
|
|
codes: []string{
|
|
// General codes.
|
|
"104", "108", "301", "320", "601", "602", "603", "3025", "3198",
|
|
// External card PS codes.
|
|
"10000", "10100", "10104", "10105", "10107", "10202", "102051", "10301", "105012", "10505", "10601", "10602", "10603",
|
|
// External alternate PS codes.
|
|
"20000", "20100", "20104", "20105", "20202", "20301", "20304", "20601", "20602", "20603",
|
|
},
|
|
},
|
|
// GTX "retry later / limits / period restrictions" style codes.
|
|
{
|
|
strategy: payoutRetryStrategyDelayed,
|
|
retryable: true,
|
|
codes: []string{
|
|
// General codes.
|
|
"312", "314", "315", "316", "325", "2466",
|
|
"3106", "3108", "3109", "3110", "3111", "3112",
|
|
"3285", "3297", "3298",
|
|
"3305", "3306", "3307", "3308", "3309", "3310", "3311", "3312", "3313", "3314", "3315", "3316", "3317", "3318", "3319", "3320", "3321", "3322", "3323", "3324", "3325", "3326", "3327", "3328", "3329", "3330", "3331", "3332", "3333", "3334", "3335", "3336", "3337", "3338", "3339", "3340",
|
|
"3342", "3343", "3344", "3345", "3346", "3347", "3348", "3349", "3350", "3351", "3352", "3353", "3355", "3357",
|
|
"3407", "3408", "3450", "3451", "3452", "3613",
|
|
// External card PS codes.
|
|
"10101", "10109", "10112", "10114", "101012", "101013", "101014",
|
|
// External alternate PS codes.
|
|
"20109", "20206", "20505", "201012", "201013", "201014",
|
|
},
|
|
},
|
|
// GTX status refresh/polling conditions.
|
|
{
|
|
strategy: payoutRetryStrategyStatusRefresh,
|
|
retryable: true,
|
|
codes: []string{
|
|
"3061", "3062",
|
|
"9999", "19999", "20802", "29999",
|
|
},
|
|
},
|
|
}
|
|
|
|
var providerDocumentedNonRetryCodes = buildDocumentedNonRetryCodes(providerDocumentedCodes, providerRetryOnlyCodeBuckets)
|
|
|
|
var providerRetryCodeBuckets = func() []retryCodeBucket {
|
|
buckets := make([]retryCodeBucket, 0, len(providerRetryOnlyCodeBuckets)+1)
|
|
buckets = append(buckets, providerRetryOnlyCodeBuckets...)
|
|
buckets = append(buckets, retryCodeBucket{
|
|
strategy: payoutRetryStrategyImmediate,
|
|
retryable: false,
|
|
codes: providerDocumentedNonRetryCodes,
|
|
})
|
|
return buckets
|
|
}()
|
|
|
|
func defaultPayoutFailurePolicy() payoutFailurePolicy {
|
|
strategies := map[string]payoutRetryStrategy{}
|
|
for _, bucket := range providerRetryCodeBuckets {
|
|
if !bucket.retryable {
|
|
continue
|
|
}
|
|
registerRetryStrategy(strategies, bucket.strategy, bucket.codes...)
|
|
}
|
|
|
|
return payoutFailurePolicy{
|
|
providerCodeStrategies: strategies,
|
|
documentedProviderCodes: newCodeSet(providerDocumentedCodes),
|
|
}
|
|
}
|
|
|
|
func (p payoutFailurePolicy) decideProviderFailure(code string) payoutFailureDecision {
|
|
normalized := normalizeProviderCode(code)
|
|
if normalized == "" {
|
|
return payoutFailureDecision{
|
|
Action: payoutFailureActionFail,
|
|
Strategy: payoutRetryStrategyImmediate,
|
|
Reason: "provider_failure",
|
|
}
|
|
}
|
|
if strategy, ok := p.providerCodeStrategies[normalized]; ok {
|
|
return payoutFailureDecision{
|
|
Action: payoutFailureActionRetry,
|
|
Strategy: strategy,
|
|
Reason: "provider_code_" + normalized,
|
|
}
|
|
}
|
|
if _, ok := p.documentedProviderCodes[normalized]; ok {
|
|
return payoutFailureDecision{
|
|
Action: payoutFailureActionFail,
|
|
Strategy: payoutRetryStrategyImmediate,
|
|
Reason: "provider_code_" + normalized + "_documented_non_retry",
|
|
}
|
|
}
|
|
return payoutFailureDecision{
|
|
Action: payoutFailureActionFail,
|
|
Strategy: payoutRetryStrategyImmediate,
|
|
Reason: "provider_code_" + normalized + "_unknown",
|
|
}
|
|
}
|
|
|
|
func (p payoutFailurePolicy) decideTransportFailure() payoutFailureDecision {
|
|
return payoutFailureDecision{
|
|
Action: payoutFailureActionRetry,
|
|
Strategy: payoutRetryStrategyImmediate,
|
|
Reason: "transport_failure",
|
|
}
|
|
}
|
|
|
|
func payoutFailureReason(code, message string) string {
|
|
cleanCode := strings.TrimSpace(code)
|
|
cleanMessage := strings.TrimSpace(message)
|
|
switch {
|
|
case cleanCode != "" && cleanMessage != "":
|
|
return cleanCode + ": " + cleanMessage
|
|
case cleanCode != "":
|
|
return cleanCode
|
|
default:
|
|
return cleanMessage
|
|
}
|
|
}
|
|
|
|
func retryDelayForAttempt(attempt uint32, strategy payoutRetryStrategy) int {
|
|
strategy = normalizeRetryStrategy(strategy)
|
|
|
|
// Backoff in seconds by strategy and attempt number (attempt starts at 1).
|
|
if strategy == payoutRetryStrategyStatusRefresh {
|
|
switch {
|
|
case attempt <= 1:
|
|
return 10
|
|
case attempt == 2:
|
|
return 20
|
|
case attempt == 3:
|
|
return 40
|
|
case attempt == 4:
|
|
return 80
|
|
default:
|
|
return 160
|
|
}
|
|
}
|
|
|
|
if strategy == payoutRetryStrategyDelayed {
|
|
switch {
|
|
case attempt <= 1:
|
|
return 30
|
|
case attempt == 2:
|
|
return 120
|
|
case attempt == 3:
|
|
return 600
|
|
case attempt == 4:
|
|
return 1800
|
|
default:
|
|
return 7200
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case attempt <= 1:
|
|
return 5
|
|
case attempt == 2:
|
|
return 15
|
|
case attempt == 3:
|
|
return 30
|
|
default:
|
|
return 60
|
|
}
|
|
}
|
|
|
|
func registerRetryStrategy(dst map[string]payoutRetryStrategy, strategy payoutRetryStrategy, codes ...string) {
|
|
if dst == nil || len(codes) == 0 {
|
|
return
|
|
}
|
|
strategy = normalizeRetryStrategy(strategy)
|
|
for _, code := range codes {
|
|
normalized := normalizeProviderCode(code)
|
|
if normalized == "" {
|
|
continue
|
|
}
|
|
dst[normalized] = strategy
|
|
}
|
|
}
|
|
|
|
func newCodeSet(codes []string) map[string]struct{} {
|
|
set := map[string]struct{}{}
|
|
for _, code := range codes {
|
|
normalized := normalizeProviderCode(code)
|
|
if normalized == "" {
|
|
continue
|
|
}
|
|
set[normalized] = struct{}{}
|
|
}
|
|
return set
|
|
}
|
|
|
|
func buildDocumentedNonRetryCodes(documented []string, retryBuckets []retryCodeBucket) []string {
|
|
documentedSet := newCodeSet(documented)
|
|
retrySet := map[string]struct{}{}
|
|
for _, bucket := range retryBuckets {
|
|
for _, code := range bucket.codes {
|
|
normalized := normalizeProviderCode(code)
|
|
if normalized == "" {
|
|
continue
|
|
}
|
|
retrySet[normalized] = struct{}{}
|
|
}
|
|
}
|
|
|
|
nonRetry := make([]string, 0, len(documentedSet))
|
|
for code := range documentedSet {
|
|
if _, ok := retrySet[code]; ok {
|
|
continue
|
|
}
|
|
nonRetry = append(nonRetry, code)
|
|
}
|
|
|
|
sort.Slice(nonRetry, func(i, j int) bool {
|
|
left, leftErr := strconv.Atoi(nonRetry[i])
|
|
right, rightErr := strconv.Atoi(nonRetry[j])
|
|
if leftErr != nil || rightErr != nil {
|
|
return nonRetry[i] < nonRetry[j]
|
|
}
|
|
return left < right
|
|
})
|
|
|
|
return nonRetry
|
|
}
|
|
|
|
func normalizeProviderCode(code string) string {
|
|
return strings.TrimSpace(code)
|
|
}
|
|
|
|
func normalizeRetryStrategy(strategy payoutRetryStrategy) payoutRetryStrategy {
|
|
switch strategy {
|
|
case payoutRetryStrategyDelayed, payoutRetryStrategyStatusRefresh:
|
|
return strategy
|
|
default:
|
|
return payoutRetryStrategyImmediate
|
|
}
|
|
}
|
|
|
|
func (s payoutRetryStrategy) String() string {
|
|
switch normalizeRetryStrategy(s) {
|
|
case payoutRetryStrategyDelayed:
|
|
return "delayed"
|
|
case payoutRetryStrategyStatusRefresh:
|
|
return "status_refresh"
|
|
default:
|
|
return "immediate"
|
|
}
|
|
}
|