fixed fee direction
This commit is contained in:
78
api/gateway/aurora/internal/service/provider/config.go
Normal file
78
api/gateway/aurora/internal/service/provider/config.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultRequestTimeout = 15 * time.Second
|
||||
DefaultStatusSuccess = "success"
|
||||
DefaultStatusProcessing = "processing"
|
||||
|
||||
OutcomeSuccess = "success"
|
||||
OutcomeProcessing = "processing"
|
||||
OutcomeDecline = "decline"
|
||||
)
|
||||
|
||||
// Config holds resolved settings for communicating with Aurora.
|
||||
type Config struct {
|
||||
BaseURL string
|
||||
ProjectID int64
|
||||
SecretKey string
|
||||
AllowedCurrencies []string
|
||||
RequireCustomerAddress bool
|
||||
RequestTimeout time.Duration
|
||||
StatusSuccess string
|
||||
StatusProcessing string
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
RequestTimeout: DefaultRequestTimeout,
|
||||
StatusSuccess: DefaultStatusSuccess,
|
||||
StatusProcessing: DefaultStatusProcessing,
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) timeout() time.Duration {
|
||||
if c.RequestTimeout <= 0 {
|
||||
return DefaultRequestTimeout
|
||||
}
|
||||
return c.RequestTimeout
|
||||
}
|
||||
|
||||
// Timeout exposes the configured HTTP timeout for external callers.
|
||||
func (c Config) Timeout() time.Duration {
|
||||
return c.timeout()
|
||||
}
|
||||
|
||||
func (c Config) CurrencyAllowed(code string) bool {
|
||||
code = strings.ToUpper(strings.TrimSpace(code))
|
||||
if code == "" {
|
||||
return false
|
||||
}
|
||||
if len(c.AllowedCurrencies) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, allowed := range c.AllowedCurrencies {
|
||||
if strings.EqualFold(strings.TrimSpace(allowed), code) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c Config) SuccessStatus() string {
|
||||
if strings.TrimSpace(c.StatusSuccess) == "" {
|
||||
return DefaultStatusSuccess
|
||||
}
|
||||
return strings.ToLower(strings.TrimSpace(c.StatusSuccess))
|
||||
}
|
||||
|
||||
func (c Config) ProcessingStatus() string {
|
||||
if strings.TrimSpace(c.StatusProcessing) == "" {
|
||||
return DefaultStatusProcessing
|
||||
}
|
||||
return strings.ToLower(strings.TrimSpace(c.StatusProcessing))
|
||||
}
|
||||
21
api/gateway/aurora/internal/service/provider/mask.go
Normal file
21
api/gateway/aurora/internal/service/provider/mask.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package provider
|
||||
|
||||
import "strings"
|
||||
|
||||
// MaskPAN redacts a primary account number by keeping the first 6 and last 4 digits.
|
||||
func MaskPAN(pan string) string {
|
||||
p := strings.TrimSpace(pan)
|
||||
if len(p) <= 4 {
|
||||
return strings.Repeat("*", len(p))
|
||||
}
|
||||
|
||||
if len(p) <= 10 {
|
||||
return p[:2] + strings.Repeat("*", len(p)-4) + p[len(p)-2:]
|
||||
}
|
||||
|
||||
maskLen := len(p) - 10
|
||||
if maskLen < 0 {
|
||||
maskLen = 0
|
||||
}
|
||||
return p[:6] + strings.Repeat("*", maskLen) + p[len(p)-4:]
|
||||
}
|
||||
23
api/gateway/aurora/internal/service/provider/mask_test.go
Normal file
23
api/gateway/aurora/internal/service/provider/mask_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package provider
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMaskPAN(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{input: "1234", expected: "****"},
|
||||
{input: "1234567890", expected: "12******90"},
|
||||
{input: "1234567890123456", expected: "123456******3456"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
got := MaskPAN(tc.input)
|
||||
if got != tc.expected {
|
||||
t.Fatalf("expected %q, got %q", tc.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
39
api/gateway/aurora/internal/service/provider/metrics.go
Normal file
39
api/gateway/aurora/internal/service/provider/metrics.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
metricsOnce sync.Once
|
||||
|
||||
cardPayoutCallbacks *prometheus.CounterVec
|
||||
)
|
||||
|
||||
func initMetrics() {
|
||||
metricsOnce.Do(func() {
|
||||
cardPayoutCallbacks = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "sendico",
|
||||
Subsystem: "aurora_gateway",
|
||||
Name: "card_payout_callbacks_total",
|
||||
Help: "Aurora card payout callbacks grouped by provider status.",
|
||||
}, []string{"status"})
|
||||
})
|
||||
}
|
||||
|
||||
// ObserveCallback records callback status for Aurora card payouts.
|
||||
func ObserveCallback(status string) {
|
||||
initMetrics()
|
||||
status = strings.TrimSpace(status)
|
||||
if status == "" {
|
||||
status = "unknown"
|
||||
}
|
||||
status = strings.ToLower(status)
|
||||
if cardPayoutCallbacks != nil {
|
||||
cardPayoutCallbacks.WithLabelValues(status).Inc()
|
||||
}
|
||||
}
|
||||
11
api/gateway/aurora/internal/service/provider/payloads.go
Normal file
11
api/gateway/aurora/internal/service/provider/payloads.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package provider
|
||||
|
||||
// CardPayoutSendResult is the minimal provider result contract used by Aurora simulator.
|
||||
type CardPayoutSendResult struct {
|
||||
Accepted bool
|
||||
ProviderRequestID string
|
||||
ProviderStatus string
|
||||
StatusCode int
|
||||
ErrorCode string
|
||||
ErrorMessage string
|
||||
}
|
||||
112
api/gateway/aurora/internal/service/provider/signature.go
Normal file
112
api/gateway/aurora/internal/service/provider/signature.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func signPayload(payload any, secret string) (string, error) {
|
||||
canonical, err := signaturePayloadString(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mac := hmac.New(sha512.New, []byte(secret))
|
||||
if _, err := mac.Write([]byte(canonical)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(mac.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// SignPayload exposes signature calculation for callback verification.
|
||||
func SignPayload(payload any, secret string) (string, error) {
|
||||
return signPayload(payload, secret)
|
||||
}
|
||||
|
||||
func signaturePayloadString(payload any) (string, error) {
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var root any
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.UseNumber()
|
||||
if err := decoder.Decode(&root); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lines := make([]string, 0)
|
||||
collectSignatureLines(nil, root, &lines)
|
||||
sort.Strings(lines)
|
||||
|
||||
return strings.Join(lines, ";"), nil
|
||||
}
|
||||
|
||||
func collectSignatureLines(path []string, value any, lines *[]string) {
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
for key, child := range v {
|
||||
if strings.EqualFold(key, "signature") {
|
||||
continue
|
||||
}
|
||||
collectSignatureLines(append(path, key), child, lines)
|
||||
}
|
||||
case []any:
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
for idx, child := range v {
|
||||
collectSignatureLines(append(path, strconv.Itoa(idx)), child, lines)
|
||||
}
|
||||
default:
|
||||
line := formatSignatureLine(path, v)
|
||||
if line != "" {
|
||||
*lines = append(*lines, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func formatSignatureLine(path []string, value any) string {
|
||||
if len(path) == 0 {
|
||||
return ""
|
||||
}
|
||||
val := signatureValueString(value)
|
||||
segments := append(append([]string{}, path...), val)
|
||||
return strings.Join(segments, ":")
|
||||
}
|
||||
|
||||
func signatureValueString(value any) string {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
return "null"
|
||||
case string:
|
||||
return v
|
||||
case json.Number:
|
||||
return v.String()
|
||||
case bool:
|
||||
if v {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
case float64:
|
||||
return strconv.FormatFloat(v, 'f', -1, 64)
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(v), 'f', -1, 32)
|
||||
case int:
|
||||
return strconv.Itoa(v)
|
||||
case int8, int16, int32, int64:
|
||||
return fmt.Sprint(v)
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return fmt.Sprint(v)
|
||||
default:
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
}
|
||||
211
api/gateway/aurora/internal/service/provider/signature_test.go
Normal file
211
api/gateway/aurora/internal/service/provider/signature_test.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package provider
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSignaturePayloadString_Example(t *testing.T) {
|
||||
payload := map[string]any{
|
||||
"general": map[string]any{
|
||||
"project_id": 3254,
|
||||
"payment_id": "id_38202316",
|
||||
"signature": "<ignored>",
|
||||
},
|
||||
"customer": map[string]any{
|
||||
"id": "585741",
|
||||
"email": "johndoe@example.com",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"address": "Downing str., 23",
|
||||
"identify": map[string]any{
|
||||
"doc_number": "54122312544",
|
||||
},
|
||||
"ip_address": "198.51.100.47",
|
||||
},
|
||||
"payment": map[string]any{
|
||||
"amount": 10800,
|
||||
"currency": "USD",
|
||||
"description": "Computer keyboards",
|
||||
},
|
||||
"receipt_data": map[string]any{
|
||||
"positions": []any{
|
||||
map[string]any{
|
||||
"quantity": "10",
|
||||
"amount": "108",
|
||||
"description": "Computer keyboard",
|
||||
},
|
||||
},
|
||||
},
|
||||
"return_url": map[string]any{
|
||||
"success": "https://paymentpage.example.com/complete-redirect?id=success",
|
||||
"decline": "https://paymentpage.example.com/complete-redirect?id=decline",
|
||||
},
|
||||
}
|
||||
|
||||
got, err := signaturePayloadString(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build signature string: %v", err)
|
||||
}
|
||||
|
||||
expected := "customer:address:Downing str., 23;customer:email:johndoe@example.com;customer:first_name:John;customer:id:585741;customer:identify:doc_number:54122312544;customer:ip_address:198.51.100.47;customer:last_name:Doe;general:payment_id:id_38202316;general:project_id:3254;payment:amount:10800;payment:currency:USD;payment:description:Computer keyboards;receipt_data:positions:0:amount:108;receipt_data:positions:0:description:Computer keyboard;receipt_data:positions:0:quantity:10;return_url:decline:https://paymentpage.example.com/complete-redirect?id=decline;return_url:success:https://paymentpage.example.com/complete-redirect?id=success"
|
||||
if got != expected {
|
||||
t.Fatalf("unexpected signature string\nexpected: %s\ngot: %s", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignPayload_Example(t *testing.T) {
|
||||
payload := map[string]any{
|
||||
"general": map[string]any{
|
||||
"project_id": 3254,
|
||||
"payment_id": "id_38202316",
|
||||
"signature": "<ignored>",
|
||||
},
|
||||
"customer": map[string]any{
|
||||
"id": "585741",
|
||||
"email": "johndoe@example.com",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"address": "Downing str., 23",
|
||||
"identify": map[string]any{
|
||||
"doc_number": "54122312544",
|
||||
},
|
||||
"ip_address": "198.51.100.47",
|
||||
},
|
||||
"payment": map[string]any{
|
||||
"amount": 10800,
|
||||
"currency": "USD",
|
||||
"description": "Computer keyboards",
|
||||
},
|
||||
"receipt_data": map[string]any{
|
||||
"positions": []any{
|
||||
map[string]any{
|
||||
"quantity": "10",
|
||||
"amount": "108",
|
||||
"description": "Computer keyboard",
|
||||
},
|
||||
},
|
||||
},
|
||||
"return_url": map[string]any{
|
||||
"success": "https://paymentpage.example.com/complete-redirect?id=success",
|
||||
"decline": "https://paymentpage.example.com/complete-redirect?id=decline",
|
||||
},
|
||||
}
|
||||
|
||||
got, err := SignPayload(payload, "secret")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to sign payload: %v", err)
|
||||
}
|
||||
expected := "lagSnuspAn+F6XkmQISqwtBg0PsiTy62fF9x33TM+278mnufIDZyi1yP0BQALuCxyikkIxIMbodBn2F8hMdRwA=="
|
||||
if got != expected {
|
||||
t.Fatalf("unexpected signature\nexpected: %s\ngot: %s", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignaturePayloadString_BooleansAndArrays(t *testing.T) {
|
||||
payload := map[string]any{
|
||||
"flag": true,
|
||||
"false_flag": false,
|
||||
"empty": "",
|
||||
"zero": 0,
|
||||
"nested": map[string]any{
|
||||
"list": []any{},
|
||||
"items": []any{"alpha", "beta"},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := signaturePayloadString(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build signature string: %v", err)
|
||||
}
|
||||
|
||||
expected := "empty:;false_flag:0;flag:1;nested:items:0:alpha;nested:items:1:beta;zero:0"
|
||||
if got != expected {
|
||||
t.Fatalf("unexpected signature string\nexpected: %s\ngot: %s", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignPayload_EthEstimateGasExample(t *testing.T) {
|
||||
payload := map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"method": "eth_estimateGas",
|
||||
"params": []any{
|
||||
map[string]any{
|
||||
"from": "0xfa89b4d534bdeb2713d4ffd893e79d6535fb58f8",
|
||||
"to": "0x44162e39eefd9296231e772663a92e72958e182f",
|
||||
"gasPrice": "0x64",
|
||||
"data": "0xa9059cbb00000000000000000000000044162e39eefd9296231e772663a92e72958e182f00000000000000000000000000000000000000000000000000000000000f4240",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := SignPayload(payload, "1")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to sign payload: %v", err)
|
||||
}
|
||||
expected := "C4WbSvXKSMyX8yLamQcUe/Nzr6nSt9m3HYn4jHSyA7yi/FaTiqk0r8BlfIzfxSCoDaRgrSd82ihgZW+DxELhdQ=="
|
||||
if got != expected {
|
||||
t.Fatalf("unexpected signature\nexpected: %s\ngot: %s", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignPayload_AuroraCallbackExample(t *testing.T) {
|
||||
payload := map[string]any{
|
||||
"customer": map[string]any{
|
||||
"id": "694ece88df756c2672dc6ce8",
|
||||
},
|
||||
"account": map[string]any{
|
||||
"number": "220070******0161",
|
||||
"type": "mir",
|
||||
"card_holder": "STEPHAN",
|
||||
"expiry_month": "03",
|
||||
"expiry_year": "2030",
|
||||
},
|
||||
"project_id": 157432,
|
||||
"payment": map[string]any{
|
||||
"id": "6952d0b307d2916aba87d4e8",
|
||||
"type": "payout",
|
||||
"status": "success",
|
||||
"date": "2025-12-29T19:04:24+0000",
|
||||
"method": "card",
|
||||
"sum": map[string]any{
|
||||
"amount": 10849,
|
||||
"currency": "RUB",
|
||||
},
|
||||
"description": "",
|
||||
},
|
||||
"operation": map[string]any{
|
||||
"sum_initial": map[string]any{
|
||||
"amount": 10849,
|
||||
"currency": "RUB",
|
||||
},
|
||||
"sum_converted": map[string]any{
|
||||
"amount": 10849,
|
||||
"currency": "RUB",
|
||||
},
|
||||
"code": "0",
|
||||
"message": "Success",
|
||||
"provider": map[string]any{
|
||||
"id": 26226,
|
||||
"payment_id": "a3761838-eabc-4c65-aa36-c854c47a226b",
|
||||
"auth_code": "",
|
||||
"endpoint_id": 26226,
|
||||
"date": "2025-12-29T19:04:23+0000",
|
||||
},
|
||||
"id": int64(5089807000008124),
|
||||
"type": "payout",
|
||||
"status": "success",
|
||||
"date": "2025-12-29T19:04:24+0000",
|
||||
"created_date": "2025-12-29T19:04:21+0000",
|
||||
"request_id": "7c3032f00629c94ad78e399c87da936f1cdc30de-2559ba11d6958d558a9f8ab8c20474d33061c654-05089808",
|
||||
},
|
||||
"signature": "IBgtwCoxhMUxF15q8DLc7orYOIJomeiaNpWs8JHHsdDYPKJsIKn4T+kYavPnKTO+yibhCLNKeL+hk2oWg9wPCQ==",
|
||||
}
|
||||
|
||||
got, err := SignPayload(payload, "1")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to sign payload: %v", err)
|
||||
}
|
||||
expected := "IBgtwCoxhMUxF15q8DLc7orYOIJomeiaNpWs8JHHsdDYPKJsIKn4T+kYavPnKTO+yibhCLNKeL+hk2oWg9wPCQ=="
|
||||
if got != expected {
|
||||
t.Fatalf("unexpected signature\nexpected: %s\ngot: %s", expected, got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user