year normalization
This commit is contained in:
@@ -156,6 +156,7 @@ func (s *Service) startDiscoveryAnnouncer() {
|
|||||||
Operations: discovery.CardPayoutRailGatewayOperations(),
|
Operations: discovery.CardPayoutRailGatewayOperations(),
|
||||||
InvokeURI: s.invokeURI,
|
InvokeURI: s.invokeURI,
|
||||||
Version: appversion.Create().Short(),
|
Version: appversion.Create().Short(),
|
||||||
|
InstanceID: discovery.InstanceID(),
|
||||||
}
|
}
|
||||||
if s.gatewayDescriptor != nil {
|
if s.gatewayDescriptor != nil {
|
||||||
if id := strings.TrimSpace(s.gatewayDescriptor.GetId()); id != "" {
|
if id := strings.TrimSpace(s.gatewayDescriptor.GetId()); id != "" {
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ type CardTokenizeRequest struct {
|
|||||||
type CardPayoutSendResult struct {
|
type CardPayoutSendResult struct {
|
||||||
Accepted bool
|
Accepted bool
|
||||||
ProviderRequestID string
|
ProviderRequestID string
|
||||||
|
ProviderStatus string
|
||||||
StatusCode int
|
StatusCode int
|
||||||
ErrorCode string
|
ErrorCode string
|
||||||
ErrorMessage string
|
ErrorMessage string
|
||||||
@@ -85,6 +86,7 @@ type TokenizationResult struct {
|
|||||||
|
|
||||||
type APIResponse struct {
|
type APIResponse struct {
|
||||||
RequestID string `json:"request_id"`
|
RequestID string `json:"request_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Operation struct {
|
Operation struct {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func (c *Client) sendCardPayout(ctx context.Context, req CardPayoutRequest) (*Ca
|
|||||||
zap.String("payout_id", req.General.PaymentID),
|
zap.String("payout_id", req.General.PaymentID),
|
||||||
zap.Bool("accepted", r.Accepted),
|
zap.Bool("accepted", r.Accepted),
|
||||||
zap.Int("status_code", r.StatusCode),
|
zap.Int("status_code", r.StatusCode),
|
||||||
|
zap.String("provider_status", r.ProviderStatus),
|
||||||
zap.String("provider_request_id", r.ProviderRequestID),
|
zap.String("provider_request_id", r.ProviderRequestID),
|
||||||
zap.String("error_code", r.ErrorCode),
|
zap.String("error_code", r.ErrorCode),
|
||||||
zap.String("error_message", r.ErrorMessage),
|
zap.String("error_message", r.ErrorMessage),
|
||||||
@@ -60,6 +61,7 @@ func (c *Client) sendCardTokenPayout(ctx context.Context, req CardTokenPayoutReq
|
|||||||
zap.String("payout_id", req.General.PaymentID),
|
zap.String("payout_id", req.General.PaymentID),
|
||||||
zap.Bool("accepted", r.Accepted),
|
zap.Bool("accepted", r.Accepted),
|
||||||
zap.Int("status_code", r.StatusCode),
|
zap.Int("status_code", r.StatusCode),
|
||||||
|
zap.String("provider_status", r.ProviderStatus),
|
||||||
zap.String("provider_request_id", r.ProviderRequestID),
|
zap.String("provider_request_id", r.ProviderRequestID),
|
||||||
zap.String("error_code", r.ErrorCode),
|
zap.String("error_code", r.ErrorCode),
|
||||||
zap.String("error_message", r.ErrorMessage),
|
zap.String("error_message", r.ErrorMessage),
|
||||||
@@ -81,6 +83,7 @@ func (c *Client) sendTokenization(ctx context.Context, req CardTokenizeRequest)
|
|||||||
if strings.TrimSpace(c.cfg.BaseURL) == "" {
|
if strings.TrimSpace(c.cfg.BaseURL) == "" {
|
||||||
return nil, merrors.Internal("monetix base url not configured")
|
return nil, merrors.Internal("monetix base url not configured")
|
||||||
}
|
}
|
||||||
|
normalizeRequestExpiryYear(&req)
|
||||||
|
|
||||||
req.General.Signature = ""
|
req.General.Signature = ""
|
||||||
signature, err := signPayload(req, c.cfg.SecretKey)
|
signature, err := signPayload(req, c.cfg.SecretKey)
|
||||||
@@ -168,6 +171,10 @@ func (c *Client) sendTokenization(ctx context.Context, req CardTokenizeRequest)
|
|||||||
} else if apiResp.RequestID != "" {
|
} else if apiResp.RequestID != "" {
|
||||||
result.ProviderRequestID = apiResp.RequestID
|
result.ProviderRequestID = apiResp.RequestID
|
||||||
}
|
}
|
||||||
|
result.ProviderStatus = strings.TrimSpace(apiResp.Status)
|
||||||
|
if result.ProviderStatus == "" {
|
||||||
|
result.ProviderStatus = strings.TrimSpace(apiResp.Operation.Status)
|
||||||
|
}
|
||||||
|
|
||||||
if !result.Accepted {
|
if !result.Accepted {
|
||||||
result.ErrorCode = apiResp.Code
|
result.ErrorCode = apiResp.Code
|
||||||
@@ -184,6 +191,7 @@ func (c *Client) sendTokenization(ctx context.Context, req CardTokenizeRequest)
|
|||||||
zap.String("request_id", req.General.PaymentID),
|
zap.String("request_id", req.General.PaymentID),
|
||||||
zap.Bool("accepted", result.Accepted),
|
zap.Bool("accepted", result.Accepted),
|
||||||
zap.Int("status_code", resp.StatusCode),
|
zap.Int("status_code", resp.StatusCode),
|
||||||
|
zap.String("provider_status", result.ProviderStatus),
|
||||||
zap.String("provider_request_id", result.ProviderRequestID),
|
zap.String("provider_request_id", result.ProviderRequestID),
|
||||||
zap.String("error_code", result.ErrorCode),
|
zap.String("error_code", result.ErrorCode),
|
||||||
zap.String("error_message", result.ErrorMessage),
|
zap.String("error_message", result.ErrorMessage),
|
||||||
@@ -205,6 +213,7 @@ func (c *Client) send(ctx context.Context, req any, path string, dispatchLog fun
|
|||||||
if strings.TrimSpace(c.cfg.BaseURL) == "" {
|
if strings.TrimSpace(c.cfg.BaseURL) == "" {
|
||||||
return nil, merrors.Internal("monetix base url not configured")
|
return nil, merrors.Internal("monetix base url not configured")
|
||||||
}
|
}
|
||||||
|
normalizeRequestExpiryYear(req)
|
||||||
|
|
||||||
setSignature, err := clearSignature(req)
|
setSignature, err := clearSignature(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -254,12 +263,16 @@ func (c *Client) send(ctx context.Context, req any, path string, dispatchLog fun
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
observeRequest(outcomeNetworkError, duration)
|
||||||
|
c.logger.Warn("Failed to read Monetix response", zap.Int("status_code", resp.StatusCode), zap.Error(err))
|
||||||
|
return nil, merrors.Internal("failed to read monetix response: " + err.Error())
|
||||||
|
}
|
||||||
outcome := outcomeAccepted
|
outcome := outcomeAccepted
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
outcome = outcomeHTTPError
|
outcome = outcomeHTTPError
|
||||||
}
|
}
|
||||||
observeRequest(outcome, duration)
|
|
||||||
|
|
||||||
result := &CardPayoutSendResult{
|
result := &CardPayoutSendResult{
|
||||||
Accepted: resp.StatusCode >= 200 && resp.StatusCode < 300,
|
Accepted: resp.StatusCode >= 200 && resp.StatusCode < 300,
|
||||||
@@ -269,7 +282,9 @@ func (c *Client) send(ctx context.Context, req any, path string, dispatchLog fun
|
|||||||
var apiResp APIResponse
|
var apiResp APIResponse
|
||||||
if len(body) > 0 {
|
if len(body) > 0 {
|
||||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||||
|
observeRequest(outcomeHTTPError, duration)
|
||||||
c.logger.Warn("Failed to decode Monetix response", zap.Int("status_code", resp.StatusCode), zap.Error(err))
|
c.logger.Warn("Failed to decode Monetix response", zap.Int("status_code", resp.StatusCode), zap.Error(err))
|
||||||
|
return nil, merrors.Internal("failed to decode monetix response: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +293,10 @@ func (c *Client) send(ctx context.Context, req any, path string, dispatchLog fun
|
|||||||
} else if apiResp.RequestID != "" {
|
} else if apiResp.RequestID != "" {
|
||||||
result.ProviderRequestID = apiResp.RequestID
|
result.ProviderRequestID = apiResp.RequestID
|
||||||
}
|
}
|
||||||
|
result.ProviderStatus = strings.TrimSpace(apiResp.Status)
|
||||||
|
if result.ProviderStatus == "" {
|
||||||
|
result.ProviderStatus = strings.TrimSpace(apiResp.Operation.Status)
|
||||||
|
}
|
||||||
|
|
||||||
if !result.Accepted {
|
if !result.Accepted {
|
||||||
result.ErrorCode = apiResp.Code
|
result.ErrorCode = apiResp.Code
|
||||||
@@ -293,10 +312,27 @@ func (c *Client) send(ctx context.Context, req any, path string, dispatchLog fun
|
|||||||
if responseLog != nil {
|
if responseLog != nil {
|
||||||
responseLog(result)
|
responseLog(result)
|
||||||
}
|
}
|
||||||
|
observeRequest(outcome, duration)
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeExpiryYear(year int) int {
|
||||||
|
if year > 0 && year < 100 {
|
||||||
|
return year + 2000
|
||||||
|
}
|
||||||
|
return year
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeRequestExpiryYear(req any) {
|
||||||
|
switch r := req.(type) {
|
||||||
|
case *CardPayoutRequest:
|
||||||
|
r.Card.Year = normalizeExpiryYear(r.Card.Year)
|
||||||
|
case *CardTokenizeRequest:
|
||||||
|
r.Card.Year = normalizeExpiryYear(r.Card.Year)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func clearSignature(req any) (func(string), error) {
|
func clearSignature(req any) (func(string), error) {
|
||||||
switch r := req.(type) {
|
switch r := req.(type) {
|
||||||
case *CardPayoutRequest:
|
case *CardPayoutRequest:
|
||||||
@@ -323,7 +359,7 @@ func logRequestDeadline(logger mlogger.Logger, ctx context.Context, url string)
|
|||||||
logger.Info("Monetix request context has no deadline", zap.String("url", url))
|
logger.Info("Monetix request context has no deadline", zap.String("url", url))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Info("Monetix request context deadline",
|
logger.Debug("Monetix request context deadline",
|
||||||
zap.String("url", url),
|
zap.String("url", url),
|
||||||
zap.Time("deadline", deadline),
|
zap.Time("deadline", deadline),
|
||||||
zap.Duration("time_until_deadline", time.Until(deadline)),
|
zap.Duration("time_until_deadline", time.Until(deadline)),
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
dto "github.com/prometheus/client_model/go"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,6 +85,51 @@ func TestSendCardPayout_SignsPayload(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSendCardPayout_NormalizesTwoDigitYearBeforeSend(t *testing.T) {
|
||||||
|
var captured CardPayoutRequest
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
if err := json.Unmarshal(body, &captured); err != nil {
|
||||||
|
t.Fatalf("failed to decode request: %v", err)
|
||||||
|
}
|
||||||
|
payload, _ := json.Marshal(APIResponse{})
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: io.NopCloser(bytes.NewReader(payload)),
|
||||||
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := Config{
|
||||||
|
BaseURL: "https://monetix.test",
|
||||||
|
SecretKey: "secret",
|
||||||
|
}
|
||||||
|
client := NewClient(cfg, httpClient, zap.NewNop())
|
||||||
|
|
||||||
|
req := CardPayoutRequest{
|
||||||
|
General: General{ProjectID: 1, PaymentID: "payout-1"},
|
||||||
|
Customer: Customer{
|
||||||
|
ID: "cust-1",
|
||||||
|
FirstName: "Jane",
|
||||||
|
LastName: "Doe",
|
||||||
|
IP: "203.0.113.10",
|
||||||
|
},
|
||||||
|
Payment: Payment{Amount: 1000, Currency: "RUB"},
|
||||||
|
Card: Card{PAN: "4111111111111111", Year: 30, Month: 12, CardHolder: "JANE DOE"},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.CreateCardPayout(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if captured.Card.Year != 2030 {
|
||||||
|
t.Fatalf("expected normalized year 2030, got %d", captured.Card.Year)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSendCardPayout_HTTPError(t *testing.T) {
|
func TestSendCardPayout_HTTPError(t *testing.T) {
|
||||||
httpClient := &http.Client{
|
httpClient := &http.Client{
|
||||||
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
@@ -126,3 +174,288 @@ func TestSendCardPayout_HTTPError(t *testing.T) {
|
|||||||
t.Fatalf("expected error message denied, got %q", result.ErrorMessage)
|
t.Fatalf("expected error message denied, got %q", result.ErrorMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type errorReadCloser struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e errorReadCloser) Read(_ []byte) (int, error) {
|
||||||
|
return 0, e.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e errorReadCloser) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type countingReadCloser struct {
|
||||||
|
data []byte
|
||||||
|
offset int
|
||||||
|
readBytes int
|
||||||
|
chunkSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *countingReadCloser) Read(p []byte) (int, error) {
|
||||||
|
if c.offset >= len(c.data) {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
n := c.chunkSize
|
||||||
|
if n <= 0 || n > len(p) {
|
||||||
|
n = len(p)
|
||||||
|
}
|
||||||
|
remaining := len(c.data) - c.offset
|
||||||
|
if n > remaining {
|
||||||
|
n = remaining
|
||||||
|
}
|
||||||
|
copy(p[:n], c.data[c.offset:c.offset+n])
|
||||||
|
c.offset += n
|
||||||
|
c.readBytes += n
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *countingReadCloser) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func counterValue(t *testing.T, counter prometheus.Counter) float64 {
|
||||||
|
t.Helper()
|
||||||
|
metric := &dto.Metric{}
|
||||||
|
if err := counter.Write(metric); err != nil {
|
||||||
|
t.Fatalf("failed to read counter value: %v", err)
|
||||||
|
}
|
||||||
|
return metric.GetCounter().GetValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendCardPayout_ReadBodyError(t *testing.T) {
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: errorReadCloser{err: errors.New("read failed")},
|
||||||
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := Config{
|
||||||
|
BaseURL: "https://monetix.test",
|
||||||
|
SecretKey: "secret",
|
||||||
|
}
|
||||||
|
client := NewClient(cfg, httpClient, zap.NewNop())
|
||||||
|
|
||||||
|
req := CardPayoutRequest{
|
||||||
|
General: General{ProjectID: 1, PaymentID: "payout-1"},
|
||||||
|
Customer: Customer{
|
||||||
|
ID: "cust-1",
|
||||||
|
FirstName: "Jane",
|
||||||
|
LastName: "Doe",
|
||||||
|
IP: "203.0.113.10",
|
||||||
|
},
|
||||||
|
Payment: Payment{Amount: 1000, Currency: "RUB"},
|
||||||
|
Card: Card{PAN: "4111111111111111", Year: 2030, Month: 12, CardHolder: "JANE DOE"},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.CreateCardPayout(context.Background(), req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected read error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "failed to read monetix response") {
|
||||||
|
t.Fatalf("expected wrapped read error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendCardPayout_DecodeErrorTrackedAsHTTPError(t *testing.T) {
|
||||||
|
initMetrics()
|
||||||
|
beforeAccepted := counterValue(t, cardPayoutRequests.WithLabelValues(outcomeAccepted))
|
||||||
|
beforeHTTPError := counterValue(t, cardPayoutRequests.WithLabelValues(outcomeHTTPError))
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: io.NopCloser(strings.NewReader("{")),
|
||||||
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := Config{
|
||||||
|
BaseURL: "https://monetix.test",
|
||||||
|
SecretKey: "secret",
|
||||||
|
}
|
||||||
|
client := NewClient(cfg, httpClient, zap.NewNop())
|
||||||
|
|
||||||
|
req := CardPayoutRequest{
|
||||||
|
General: General{ProjectID: 1, PaymentID: "payout-1"},
|
||||||
|
Customer: Customer{
|
||||||
|
ID: "cust-1",
|
||||||
|
FirstName: "Jane",
|
||||||
|
LastName: "Doe",
|
||||||
|
IP: "203.0.113.10",
|
||||||
|
},
|
||||||
|
Payment: Payment{Amount: 1000, Currency: "RUB"},
|
||||||
|
Card: Card{PAN: "4111111111111111", Year: 2030, Month: 12, CardHolder: "JANE DOE"},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.CreateCardPayout(context.Background(), req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected decode error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "failed to decode monetix response") {
|
||||||
|
t.Fatalf("expected wrapped decode error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
afterAccepted := counterValue(t, cardPayoutRequests.WithLabelValues(outcomeAccepted))
|
||||||
|
afterHTTPError := counterValue(t, cardPayoutRequests.WithLabelValues(outcomeHTTPError))
|
||||||
|
|
||||||
|
if afterAccepted != beforeAccepted {
|
||||||
|
t.Fatalf("accepted counter changed unexpectedly: before=%v after=%v", beforeAccepted, afterAccepted)
|
||||||
|
}
|
||||||
|
if afterHTTPError != beforeHTTPError+1 {
|
||||||
|
t.Fatalf("http_error counter not incremented: before=%v after=%v", beforeHTTPError, afterHTTPError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendCardPayout_ReadsFullMonetixRegistrationBody(t *testing.T) {
|
||||||
|
rawBody := `{"status":"waiting","request_id":"req-123","project_id":10,"payment_id":"pay-1"}`
|
||||||
|
body := &countingReadCloser{
|
||||||
|
data: []byte(rawBody),
|
||||||
|
chunkSize: 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: body,
|
||||||
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := Config{
|
||||||
|
BaseURL: "https://monetix.test",
|
||||||
|
SecretKey: "secret",
|
||||||
|
}
|
||||||
|
client := NewClient(cfg, httpClient, zap.NewNop())
|
||||||
|
|
||||||
|
req := CardPayoutRequest{
|
||||||
|
General: General{ProjectID: 1, PaymentID: "payout-1"},
|
||||||
|
Customer: Customer{
|
||||||
|
ID: "cust-1",
|
||||||
|
FirstName: "Jane",
|
||||||
|
LastName: "Doe",
|
||||||
|
IP: "203.0.113.10",
|
||||||
|
},
|
||||||
|
Payment: Payment{Amount: 1000, Currency: "RUB"},
|
||||||
|
Card: Card{PAN: "4111111111111111", Year: 2030, Month: 12, CardHolder: "JANE DOE"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := client.CreateCardPayout(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if !result.Accepted {
|
||||||
|
t.Fatalf("expected accepted response")
|
||||||
|
}
|
||||||
|
if result.ProviderRequestID != "req-123" {
|
||||||
|
t.Fatalf("expected provider request id req-123, got %q", result.ProviderRequestID)
|
||||||
|
}
|
||||||
|
if result.ProviderStatus != "waiting" {
|
||||||
|
t.Fatalf("expected provider status waiting, got %q", result.ProviderStatus)
|
||||||
|
}
|
||||||
|
if body.readBytes != len(rawBody) {
|
||||||
|
t.Fatalf("expected full body read (%d), got %d", len(rawBody), body.readBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendCardPayout_ProviderStatusFallsBackToOperationStatus(t *testing.T) {
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
body := `{"operation":{"request_id":"req-1","status":"processing"}}`
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: io.NopCloser(strings.NewReader(body)),
|
||||||
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := Config{
|
||||||
|
BaseURL: "https://monetix.test",
|
||||||
|
SecretKey: "secret",
|
||||||
|
}
|
||||||
|
client := NewClient(cfg, httpClient, zap.NewNop())
|
||||||
|
|
||||||
|
req := CardPayoutRequest{
|
||||||
|
General: General{ProjectID: 1, PaymentID: "payout-1"},
|
||||||
|
Customer: Customer{
|
||||||
|
ID: "cust-1",
|
||||||
|
FirstName: "Jane",
|
||||||
|
LastName: "Doe",
|
||||||
|
IP: "203.0.113.10",
|
||||||
|
},
|
||||||
|
Payment: Payment{Amount: 1000, Currency: "RUB"},
|
||||||
|
Card: Card{PAN: "4111111111111111", Year: 2030, Month: 12, CardHolder: "JANE DOE"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := client.CreateCardPayout(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if result.ProviderStatus != "processing" {
|
||||||
|
t.Fatalf("expected provider status processing, got %q", result.ProviderStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendCardTokenization_NormalizesTwoDigitYearBeforeSend(t *testing.T) {
|
||||||
|
var captured CardTokenizeRequest
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.URL.Path != "/v1/tokenize" {
|
||||||
|
t.Fatalf("expected tokenization path, got %q", r.URL.Path)
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
if err := json.Unmarshal(body, &captured); err != nil {
|
||||||
|
t.Fatalf("failed to decode request: %v", err)
|
||||||
|
}
|
||||||
|
payload, _ := json.Marshal(APIResponse{})
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: io.NopCloser(bytes.NewReader(payload)),
|
||||||
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := Config{
|
||||||
|
BaseURL: "https://monetix.test",
|
||||||
|
SecretKey: "secret",
|
||||||
|
}
|
||||||
|
client := NewClient(cfg, httpClient, zap.NewNop())
|
||||||
|
|
||||||
|
req := CardTokenizeRequest{
|
||||||
|
General: General{ProjectID: 1, PaymentID: "tokenize-1"},
|
||||||
|
Customer: Customer{
|
||||||
|
ID: "cust-1",
|
||||||
|
FirstName: "Jane",
|
||||||
|
LastName: "Doe",
|
||||||
|
IP: "203.0.113.10",
|
||||||
|
},
|
||||||
|
Card: CardTokenize{
|
||||||
|
PAN: "4111111111111111",
|
||||||
|
Year: 30,
|
||||||
|
Month: 12,
|
||||||
|
CardHolder: "JANE DOE",
|
||||||
|
CVV: "123",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.CreateCardTokenization(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if captured.Card.Year != 2030 {
|
||||||
|
t.Fatalf("expected normalized year 2030, got %d", captured.Card.Year)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user