mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
feat: add custom easypay payment methods
This commit is contained in:
@@ -39,6 +39,12 @@ type EasyPay struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type easyPayCustomMethod struct {
|
||||
Type string `json:"type"`
|
||||
UpstreamType string `json:"upstreamType"`
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
// NewEasyPay creates a new EasyPay provider.
|
||||
// config keys: pid, pkey, apiBase, notifyUrl, returnUrl, cid, cidAlipay, cidWxpay
|
||||
func NewEasyPay(instanceID string, config map[string]string) (*EasyPay, error) {
|
||||
@@ -95,7 +101,13 @@ func (e *EasyPay) apiBase() string {
|
||||
func (e *EasyPay) Name() string { return "EasyPay" }
|
||||
func (e *EasyPay) ProviderKey() string { return payment.TypeEasyPay }
|
||||
func (e *EasyPay) SupportedTypes() []payment.PaymentType {
|
||||
return []payment.PaymentType{payment.TypeAlipay, payment.TypeWxpay}
|
||||
types := []payment.PaymentType{payment.TypeAlipay, payment.TypeWxpay}
|
||||
for _, method := range e.customMethods() {
|
||||
if method.Type != "" {
|
||||
types = append(types, method.Type)
|
||||
}
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
func (e *EasyPay) MerchantIdentityMetadata() map[string]string {
|
||||
@@ -124,13 +136,14 @@ func (e *EasyPay) CreatePayment(ctx context.Context, req payment.CreatePaymentRe
|
||||
// TradeNo is empty; it arrives via the notify callback after payment.
|
||||
func (e *EasyPay) createRedirectPayment(req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
|
||||
notifyURL, returnURL := e.resolveURLs(req)
|
||||
paymentType := e.upstreamPaymentType(req.PaymentType)
|
||||
params := map[string]string{
|
||||
"pid": e.config["pid"], "type": req.PaymentType,
|
||||
"pid": e.config["pid"], "type": paymentType,
|
||||
"out_trade_no": req.OrderID, "notify_url": notifyURL,
|
||||
"return_url": returnURL, "name": req.Subject,
|
||||
"money": req.Amount,
|
||||
}
|
||||
if cid := e.resolveCID(req.PaymentType); cid != "" {
|
||||
if cid := e.resolveCID(paymentType); cid != "" {
|
||||
params["cid"] = cid
|
||||
}
|
||||
if req.IsMobile {
|
||||
@@ -150,13 +163,14 @@ func (e *EasyPay) createRedirectPayment(req payment.CreatePaymentRequest) (*paym
|
||||
// createAPIPayment calls mapi.php to get payurl/qrcode (existing behavior).
|
||||
func (e *EasyPay) createAPIPayment(ctx context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
|
||||
notifyURL, returnURL := e.resolveURLs(req)
|
||||
paymentType := e.upstreamPaymentType(req.PaymentType)
|
||||
params := map[string]string{
|
||||
"pid": e.config["pid"], "type": req.PaymentType,
|
||||
"pid": e.config["pid"], "type": paymentType,
|
||||
"out_trade_no": req.OrderID, "notify_url": notifyURL,
|
||||
"return_url": returnURL, "name": req.Subject,
|
||||
"money": req.Amount, "clientip": req.ClientIP,
|
||||
}
|
||||
if cid := e.resolveCID(req.PaymentType); cid != "" {
|
||||
if cid := e.resolveCID(paymentType); cid != "" {
|
||||
params["cid"] = cid
|
||||
}
|
||||
if req.IsMobile {
|
||||
@@ -204,6 +218,41 @@ func (e *EasyPay) resolveURLs(req payment.CreatePaymentRequest) (string, string)
|
||||
return notifyURL, returnURL
|
||||
}
|
||||
|
||||
func (e *EasyPay) customMethods() []easyPayCustomMethod {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
raw := strings.TrimSpace(e.config["customMethods"])
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var methods []easyPayCustomMethod
|
||||
if err := json.Unmarshal([]byte(raw), &methods); err != nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]easyPayCustomMethod, 0, len(methods))
|
||||
for _, method := range methods {
|
||||
method.Type = strings.TrimSpace(method.Type)
|
||||
method.UpstreamType = strings.TrimSpace(method.UpstreamType)
|
||||
method.DisplayName = strings.TrimSpace(method.DisplayName)
|
||||
if method.Type == "" || method.UpstreamType == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, method)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (e *EasyPay) upstreamPaymentType(paymentType string) string {
|
||||
paymentType = strings.TrimSpace(paymentType)
|
||||
for _, method := range e.customMethods() {
|
||||
if paymentType == method.Type {
|
||||
return method.UpstreamType
|
||||
}
|
||||
}
|
||||
return paymentType
|
||||
}
|
||||
|
||||
func (e *EasyPay) QueryOrder(ctx context.Context, tradeNo string) (*payment.QueryOrderResponse, error) {
|
||||
params := map[string]string{
|
||||
"act": "order", "pid": e.config["pid"],
|
||||
|
||||
@@ -179,6 +179,102 @@ func TestEasyPayRefundResponseErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEasyPayCustomMethodsUseConfiguredUpstreamType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
provider, err := NewEasyPay("test-instance", map[string]string{
|
||||
"pid": "pid-1",
|
||||
"pkey": "pkey-1",
|
||||
"apiBase": "https://pay.example.com",
|
||||
"notifyUrl": "https://example.com/notify",
|
||||
"returnUrl": "https://example.com/return",
|
||||
"paymentMode": paymentModePopup,
|
||||
"customMethods": `[{"type":"ldc","upstreamType":"epay","displayName":"LDC"},{"type":"usdt_trc20","upstreamType":"usdt","displayName":"USDT-TRC20"}]`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewEasyPay: %v", err)
|
||||
}
|
||||
|
||||
resp, err := provider.CreatePayment(context.Background(), payment.CreatePaymentRequest{
|
||||
OrderID: "sub2-custom-1",
|
||||
Amount: "1.00",
|
||||
PaymentType: "usdt_trc20",
|
||||
Subject: "Custom EasyPay",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePayment: %v", err)
|
||||
}
|
||||
payURL, err := url.Parse(resp.PayURL)
|
||||
if err != nil {
|
||||
t.Fatalf("parse pay url: %v", err)
|
||||
}
|
||||
if got := payURL.Query().Get("type"); got != "usdt" {
|
||||
t.Fatalf("pay url type = %q, want usdt (%s)", got, resp.PayURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEasyPayCustomMethodsResolveCIDFromConfiguredUpstreamType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
provider, err := NewEasyPay("test-instance", map[string]string{
|
||||
"pid": "pid-1",
|
||||
"pkey": "pkey-1",
|
||||
"apiBase": "https://pay.example.com",
|
||||
"notifyUrl": "https://example.com/notify",
|
||||
"returnUrl": "https://example.com/return",
|
||||
"paymentMode": paymentModePopup,
|
||||
"cidAlipay": "cid-alipay",
|
||||
"cidWxpay": "cid-wxpay",
|
||||
"customMethods": `[{"type":"ldc","upstreamType":"alipay","displayName":"LDC"}]`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewEasyPay: %v", err)
|
||||
}
|
||||
|
||||
resp, err := provider.CreatePayment(context.Background(), payment.CreatePaymentRequest{
|
||||
OrderID: "sub2-custom-cid",
|
||||
Amount: "1.00",
|
||||
PaymentType: "ldc",
|
||||
Subject: "Custom EasyPay CID",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePayment: %v", err)
|
||||
}
|
||||
payURL, err := url.Parse(resp.PayURL)
|
||||
if err != nil {
|
||||
t.Fatalf("parse pay url: %v", err)
|
||||
}
|
||||
if got := payURL.Query().Get("type"); got != "alipay" {
|
||||
t.Fatalf("pay url type = %q, want alipay (%s)", got, resp.PayURL)
|
||||
}
|
||||
if got := payURL.Query().Get("cid"); got != "cid-alipay" {
|
||||
t.Fatalf("pay url cid = %q, want cid-alipay (%s)", got, resp.PayURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEasyPaySupportedTypesIncludeCustomMethods(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
provider, err := NewEasyPay("test-instance", map[string]string{
|
||||
"pid": "pid-1",
|
||||
"pkey": "pkey-1",
|
||||
"apiBase": "https://pay.example.com",
|
||||
"notifyUrl": "https://example.com/notify",
|
||||
"returnUrl": "https://example.com/return",
|
||||
"customMethods": `[{"type":"ldc","upstreamType":"epay","displayName":"LDC"},{"type":"usdt_trc20","upstreamType":"usdt","displayName":"USDT-TRC20"}]`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewEasyPay: %v", err)
|
||||
}
|
||||
|
||||
got := strings.Join(provider.SupportedTypes(), ",")
|
||||
for _, want := range []string{"alipay", "wxpay", "ldc", "usdt_trc20"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("SupportedTypes() = %q, want it to include %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTestEasyPay(t *testing.T, apiBase string) *EasyPay {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
|
||||
@@ -31,6 +32,7 @@ func (s *PaymentConfigService) GetAvailableMethodLimits(ctx context.Context) (*M
|
||||
continue
|
||||
}
|
||||
ml := pcAggregateMethodLimits(pt, insts)
|
||||
ml.DisplayName = s.pcAggregateMethodDisplayName(pt, insts)
|
||||
ml.Currency = currency
|
||||
resp.Methods[ml.PaymentType] = ml
|
||||
}
|
||||
@@ -93,6 +95,7 @@ func (s *PaymentConfigService) GetMethodLimits(ctx context.Context, types []stri
|
||||
continue
|
||||
}
|
||||
ml := pcAggregateMethodLimits(pt, matching)
|
||||
ml.DisplayName = s.pcAggregateMethodDisplayName(pt, matching)
|
||||
ml.Currency = currency
|
||||
result = append(result, ml)
|
||||
}
|
||||
@@ -163,6 +166,53 @@ func (s *PaymentConfigService) pcInstancePaymentCurrency(inst *dbent.PaymentProv
|
||||
return paymentProviderConfigCurrency(inst.ProviderKey, cfg)
|
||||
}
|
||||
|
||||
type easyPayCustomMethodDisplayConfig struct {
|
||||
Type string `json:"type"`
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
func (s *PaymentConfigService) pcAggregateMethodDisplayName(pt string, instances []*dbent.PaymentProviderInstance) string {
|
||||
pt = strings.TrimSpace(pt)
|
||||
if pt == "" {
|
||||
return ""
|
||||
}
|
||||
for _, inst := range instances {
|
||||
displayName := s.pcInstanceEasyPayCustomMethodDisplayName(inst, pt)
|
||||
if displayName != "" {
|
||||
return displayName
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *PaymentConfigService) pcInstanceEasyPayCustomMethodDisplayName(inst *dbent.PaymentProviderInstance, pt string) string {
|
||||
if inst == nil || inst.ProviderKey != payment.TypeEasyPay {
|
||||
return ""
|
||||
}
|
||||
cfg := map[string]string{}
|
||||
if s != nil {
|
||||
decrypted, err := s.decryptConfig(inst.Config)
|
||||
if err == nil && decrypted != nil {
|
||||
cfg = decrypted
|
||||
}
|
||||
}
|
||||
raw := strings.TrimSpace(cfg["customMethods"])
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var methods []easyPayCustomMethodDisplayConfig
|
||||
if err := json.Unmarshal([]byte(raw), &methods); err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, method := range methods {
|
||||
if strings.TrimSpace(method.Type) == pt {
|
||||
return strings.TrimSpace(method.DisplayName)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// pcGroupByPaymentType groups instances by user-facing payment type.
|
||||
// For Stripe providers, ALL sub-types (card, link, alipay, wxpay) map to "stripe"
|
||||
// because the user sees a single "Stripe" button, not individual sub-methods.
|
||||
|
||||
@@ -255,6 +255,28 @@ func TestGetAvailableMethodLimitsOmitsMixedCurrencyMethod(t *testing.T) {
|
||||
require.Equal(t, "PAYMENT_METHOD_CURRENCY_CONFLICT", appErr.Reason)
|
||||
}
|
||||
|
||||
func TestGetAvailableMethodLimitsIncludesEasyPayCustomMethodDisplayName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := newPaymentConfigServiceTestClient(t)
|
||||
|
||||
_, err := client.PaymentProviderInstance.Create().
|
||||
SetProviderKey(payment.TypeEasyPay).
|
||||
SetName("EasyPay Custom").
|
||||
SetConfig(`{"customMethods":"[{\"type\":\"ldc\",\"upstreamType\":\"ldc\",\"displayName\":\"LDC Pay\"}]"}`).
|
||||
SetSupportedTypes("alipay,wxpay,ldc").
|
||||
SetEnabled(true).
|
||||
Save(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
svc := &PaymentConfigService{entClient: client}
|
||||
resp, err := svc.GetAvailableMethodLimits(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
limits, ok := resp.Methods["ldc"]
|
||||
require.True(t, ok, "expected custom EasyPay method limits to be visible")
|
||||
require.Equal(t, "LDC Pay", limits.DisplayName)
|
||||
}
|
||||
|
||||
func TestPcComputeGlobalRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -185,6 +186,11 @@ func (s *PaymentConfigService) CreateProviderInstance(ctx context.Context, req C
|
||||
if err := validateProviderRequest(req.ProviderKey, req.Name, typesStr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.ProviderKey == payment.TypeEasyPay {
|
||||
if err := validateEasyPayCustomMethods(req.Config, typesStr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := s.validateVisibleMethodEnablementConflicts(ctx, 0, req.ProviderKey, typesStr, req.Enabled); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -217,6 +223,64 @@ func validateProviderRequest(providerKey, name, supportedTypes string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var easyPayCustomMethodCodePattern = regexp.MustCompile(`^[a-z0-9_-]+$`)
|
||||
|
||||
type easyPayCustomMethodConfig struct {
|
||||
Type string `json:"type"`
|
||||
UpstreamType string `json:"upstreamType"`
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
func validateEasyPayCustomMethods(config map[string]string, supportedTypes string) error {
|
||||
if config == nil {
|
||||
config = map[string]string{}
|
||||
}
|
||||
raw := strings.TrimSpace(config["customMethods"])
|
||||
methods := make([]easyPayCustomMethodConfig, 0)
|
||||
if raw != "" {
|
||||
if err := json.Unmarshal([]byte(raw), &methods); err != nil {
|
||||
return infraerrors.BadRequest("VALIDATION_ERROR", "customMethods must be a JSON array")
|
||||
}
|
||||
}
|
||||
|
||||
customTypes := make(map[string]struct{}, len(methods))
|
||||
for _, method := range methods {
|
||||
method.Type = strings.TrimSpace(strings.ToLower(method.Type))
|
||||
method.UpstreamType = strings.TrimSpace(strings.ToLower(method.UpstreamType))
|
||||
if method.Type == "" || method.UpstreamType == "" {
|
||||
return infraerrors.BadRequest("VALIDATION_ERROR", "customMethods upstreamType is required")
|
||||
}
|
||||
if !easyPayCustomMethodCodePattern.MatchString(method.Type) {
|
||||
return infraerrors.BadRequest("VALIDATION_ERROR", "customMethods type may only contain lowercase letters, digits, underscores, and hyphens")
|
||||
}
|
||||
if !easyPayCustomMethodCodePattern.MatchString(method.UpstreamType) {
|
||||
return infraerrors.BadRequest("VALIDATION_ERROR", "customMethods upstreamType may only contain lowercase letters, digits, underscores, and hyphens")
|
||||
}
|
||||
if easyPayCustomMethodTypeConflictsWithBuiltin(method.Type) {
|
||||
return infraerrors.BadRequest("VALIDATION_ERROR", "customMethods type cannot start with alipay or wxpay")
|
||||
}
|
||||
if _, exists := customTypes[method.Type]; exists {
|
||||
return infraerrors.BadRequest("VALIDATION_ERROR", "duplicate customMethods type")
|
||||
}
|
||||
customTypes[method.Type] = struct{}{}
|
||||
}
|
||||
|
||||
for _, supportedType := range splitTypes(supportedTypes) {
|
||||
supportedType = strings.TrimSpace(strings.ToLower(supportedType))
|
||||
if supportedType == "" || supportedType == payment.TypeAlipay || supportedType == payment.TypeWxpay {
|
||||
continue
|
||||
}
|
||||
if _, exists := customTypes[supportedType]; !exists {
|
||||
return infraerrors.BadRequest("VALIDATION_ERROR", fmt.Sprintf("supported EasyPay custom type %s has no customMethods mapping", supportedType))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func easyPayCustomMethodTypeConflictsWithBuiltin(methodType string) bool {
|
||||
return strings.HasPrefix(methodType, payment.TypeAlipay) || strings.HasPrefix(methodType, payment.TypeWxpay)
|
||||
}
|
||||
|
||||
// UpdateProviderInstance updates a provider instance by ID (patch semantics).
|
||||
// NOTE: This function exceeds 30 lines due to per-field nil-check patch update
|
||||
// boilerplate and pending-order safety checks.
|
||||
@@ -279,6 +343,18 @@ func (s *PaymentConfigService) UpdateProviderInstance(ctx context.Context, id in
|
||||
WithMetadata(map[string]string{"count": strconv.Itoa(count)})
|
||||
}
|
||||
}
|
||||
configToValidate := mergedConfig
|
||||
if configToValidate == nil {
|
||||
configToValidate, err = s.decryptConfig(current.Config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt existing config: %w", err)
|
||||
}
|
||||
}
|
||||
if current.ProviderKey == payment.TypeEasyPay {
|
||||
if err := validateEasyPayCustomMethods(configToValidate, nextSupportedTypes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Validate merged config when the instance will end up enabled.
|
||||
// This surfaces provider-level errors (e.g. wxpay missing certSerial) at save time,
|
||||
// so admins see them in the dialog instead of only when an order is created.
|
||||
@@ -287,13 +363,6 @@ func (s *PaymentConfigService) UpdateProviderInstance(ctx context.Context, id in
|
||||
finalEnabled = *req.Enabled
|
||||
}
|
||||
if finalEnabled {
|
||||
configToValidate := mergedConfig
|
||||
if configToValidate == nil {
|
||||
configToValidate, err = s.decryptConfig(current.Config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt existing config: %w", err)
|
||||
}
|
||||
}
|
||||
if err := s.validateProviderConfig(current.ProviderKey, configToValidate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -114,6 +114,74 @@ func TestValidateProviderRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateEasyPayCustomMethods(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config map[string]string
|
||||
supportedTypes string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid custom methods",
|
||||
config: map[string]string{"customMethods": `[{"type":"ldc","upstreamType":"epay","displayName":"LDC"}]`},
|
||||
supportedTypes: "alipay,wxpay,ldc",
|
||||
},
|
||||
{
|
||||
name: "malformed custom methods json",
|
||||
config: map[string]string{"customMethods": `not-json`},
|
||||
supportedTypes: "alipay,wxpay,ldc",
|
||||
wantErr: "customMethods must be a JSON array",
|
||||
},
|
||||
{
|
||||
name: "missing upstream type",
|
||||
config: map[string]string{"customMethods": `[{"type":"ldc","displayName":"LDC"}]`},
|
||||
supportedTypes: "alipay,wxpay,ldc",
|
||||
wantErr: "customMethods upstreamType is required",
|
||||
},
|
||||
{
|
||||
name: "duplicate custom type",
|
||||
config: map[string]string{"customMethods": `[{"type":"ldc","upstreamType":"epay"},{"type":"ldc","upstreamType":"epay2"}]`},
|
||||
supportedTypes: "alipay,wxpay,ldc",
|
||||
wantErr: "duplicate customMethods type",
|
||||
},
|
||||
{
|
||||
name: "custom type uses alipay prefix",
|
||||
config: map[string]string{"customMethods": `[{"type":"alipay_hk","upstreamType":"hkpay"}]`},
|
||||
supportedTypes: "alipay,wxpay,alipay_hk",
|
||||
wantErr: "customMethods type cannot start with alipay or wxpay",
|
||||
},
|
||||
{
|
||||
name: "custom type uses wxpay prefix",
|
||||
config: map[string]string{"customMethods": `[{"type":"wxpay_usdt","upstreamType":"usdt"}]`},
|
||||
supportedTypes: "alipay,wxpay,wxpay_usdt",
|
||||
wantErr: "customMethods type cannot start with alipay or wxpay",
|
||||
},
|
||||
{
|
||||
name: "supported custom type missing mapping",
|
||||
config: map[string]string{"customMethods": `[{"type":"ldc","upstreamType":"epay"}]`},
|
||||
supportedTypes: "alipay,wxpay,ldc,usdt_trc20",
|
||||
wantErr: "supported EasyPay custom type usdt_trc20 has no customMethods mapping",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := validateEasyPayCustomMethods(tc.config, tc.supportedTypes)
|
||||
if tc.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSensitiveProviderConfigField(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ type UpdatePaymentConfigRequest struct {
|
||||
// MethodLimits holds per-payment-type limits.
|
||||
type MethodLimits struct {
|
||||
PaymentType string `json:"payment_type"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
Currency string `json:"currency"`
|
||||
FeeRate float64 `json:"fee_rate"`
|
||||
DailyLimit float64 `json:"daily_limit"`
|
||||
|
||||
@@ -187,6 +187,23 @@ func TestParsePaymentConfig(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("custom enabled types are preserved", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
vals := map[string]string{
|
||||
SettingEnabledPaymentTypes: "alipay,ldc,usdt_trc20",
|
||||
}
|
||||
cfg := svc.parsePaymentConfig(vals)
|
||||
want := []string{"alipay", "ldc", "usdt_trc20"}
|
||||
if len(cfg.EnabledTypes) != len(want) {
|
||||
t.Fatalf("EnabledTypes len = %d, want %d (%v)", len(cfg.EnabledTypes), len(want), cfg.EnabledTypes)
|
||||
}
|
||||
for i := range want {
|
||||
if cfg.EnabledTypes[i] != want[i] {
|
||||
t.Fatalf("EnabledTypes[%d] = %q, want %q (full=%v)", i, cfg.EnabledTypes[i], want[i], cfg.EnabledTypes)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty enabled types string", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
vals := map[string]string{
|
||||
|
||||
@@ -26,9 +26,10 @@ func TestNormalizeVisibleMethods(t *testing.T) {
|
||||
" wxpay_direct ",
|
||||
"wxpay",
|
||||
"stripe",
|
||||
"ldc",
|
||||
})
|
||||
|
||||
want := []string{"alipay", "wxpay", "stripe"}
|
||||
want := []string{"alipay", "wxpay", "stripe", "ldc"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("NormalizeVisibleMethods len = %d, want %d (%v)", len(got), len(want), got)
|
||||
}
|
||||
@@ -39,6 +40,21 @@ func TestNormalizeVisibleMethods(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnabledVisibleMethodsForEasyPayIncludesCustomSupportedTypes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := enabledVisibleMethodsForProvider(payment.TypeEasyPay, "alipay,ldc,usdt_trc20")
|
||||
want := []string{"alipay", "ldc", "usdt_trc20"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("enabledVisibleMethodsForProvider len = %d, want %d (%v)", len(got), len(want), got)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("enabledVisibleMethodsForProvider[%d] = %q, want %q (full=%v)", i, got[i], want[i], got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizePaymentSource(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@ func enabledVisibleMethodsForProvider(providerKey, supportedTypes string) []stri
|
||||
methodSet := make(map[string]struct{}, 2)
|
||||
addMethod := func(method string) {
|
||||
method = NormalizeVisibleMethod(method)
|
||||
switch method {
|
||||
case payment.TypeAlipay, payment.TypeWxpay:
|
||||
if method != "" {
|
||||
methodSet[method] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +54,14 @@ func enabledVisibleMethodsForProvider(providerKey, supportedTypes string) []stri
|
||||
for _, method := range []string{payment.TypeAlipay, payment.TypeWxpay} {
|
||||
if _, ok := methodSet[method]; ok {
|
||||
methods = append(methods, method)
|
||||
delete(methodSet, method)
|
||||
}
|
||||
}
|
||||
for _, supportedType := range splitTypes(supportedTypes) {
|
||||
method := NormalizeVisibleMethod(supportedType)
|
||||
if _, ok := methodSet[method]; ok {
|
||||
methods = append(methods, method)
|
||||
delete(methodSet, method)
|
||||
}
|
||||
}
|
||||
return methods
|
||||
@@ -215,7 +222,7 @@ func (s *PaymentConfigService) resolveEnabledVisibleMethodInstance(
|
||||
}
|
||||
|
||||
method = NormalizeVisibleMethod(method)
|
||||
if method != payment.TypeAlipay && method != payment.TypeWxpay {
|
||||
if method == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user