extended aurora scenarios + payment operation amounts

This commit is contained in:
Stephan D
2026-03-11 01:09:11 +01:00
parent e446486b77
commit 9ad2104d7d
46 changed files with 1057 additions and 193 deletions

View File

@@ -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.

View File

@@ -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 {

View 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
}

View 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{}{}
}
}

View File

@@ -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"))
}

View File

@@ -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 = "" },

View File

@@ -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)

View File

@@ -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"))
}

View File

@@ -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

View File

@@ -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 ""

View File

@@ -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{