Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
b22d00e5
Commit
b22d00e5
authored
Apr 21, 2026
by
IanShaw027
Browse files
feat: drive visible payment methods from enabled providers
parent
54dc1767
Changes
15
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/payment_config_limits.go
View file @
b22d00e5
...
@@ -20,18 +20,7 @@ func (s *PaymentConfigService) GetAvailableMethodLimits(ctx context.Context) (*M
...
@@ -20,18 +20,7 @@ func (s *PaymentConfigService) GetAvailableMethodLimits(ctx context.Context) (*M
return
nil
,
fmt
.
Errorf
(
"query provider instances: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"query provider instances: %w"
,
err
)
}
}
typeInstances
:=
pcGroupByPaymentType
(
instances
)
typeInstances
:=
pcGroupByPaymentType
(
instances
)
if
s
.
settingRepo
!=
nil
{
typeInstances
=
pcApplyEnabledVisibleMethodInstances
(
typeInstances
,
instances
)
vals
,
err
:=
s
.
settingRepo
.
GetMultiple
(
ctx
,
[]
string
{
SettingPaymentVisibleMethodAlipayEnabled
,
SettingPaymentVisibleMethodAlipaySource
,
SettingPaymentVisibleMethodWxpayEnabled
,
SettingPaymentVisibleMethodWxpaySource
,
})
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"query visible method settings: %w"
,
err
)
}
typeInstances
=
pcApplyVisibleMethodRouting
(
typeInstances
,
vals
,
buildVisibleMethodSourceAvailability
(
instances
))
}
resp
:=
&
MethodLimitsResponse
{
resp
:=
&
MethodLimitsResponse
{
Methods
:
make
(
map
[
string
]
MethodLimits
,
len
(
typeInstances
)),
Methods
:
make
(
map
[
string
]
MethodLimits
,
len
(
typeInstances
)),
}
}
...
@@ -43,6 +32,27 @@ func (s *PaymentConfigService) GetAvailableMethodLimits(ctx context.Context) (*M
...
@@ -43,6 +32,27 @@ func (s *PaymentConfigService) GetAvailableMethodLimits(ctx context.Context) (*M
return
resp
,
nil
return
resp
,
nil
}
}
func
pcApplyEnabledVisibleMethodInstances
(
typeInstances
map
[
string
][]
*
dbent
.
PaymentProviderInstance
,
instances
[]
*
dbent
.
PaymentProviderInstance
)
map
[
string
][]
*
dbent
.
PaymentProviderInstance
{
if
len
(
typeInstances
)
==
0
{
return
typeInstances
}
filtered
:=
make
(
map
[
string
][]
*
dbent
.
PaymentProviderInstance
,
len
(
typeInstances
))
for
paymentType
,
groupedInstances
:=
range
typeInstances
{
filtered
[
paymentType
]
=
groupedInstances
}
for
_
,
method
:=
range
[]
string
{
payment
.
TypeAlipay
,
payment
.
TypeWxpay
}
{
matching
:=
filterEnabledVisibleMethodInstances
(
instances
,
method
)
if
len
(
matching
)
!=
1
{
delete
(
filtered
,
method
)
continue
}
filtered
[
method
]
=
[]
*
dbent
.
PaymentProviderInstance
{
matching
[
0
]}
}
return
filtered
}
func
pcApplyVisibleMethodRouting
(
typeInstances
map
[
string
][]
*
dbent
.
PaymentProviderInstance
,
vals
map
[
string
]
string
,
available
map
[
string
]
bool
)
map
[
string
][]
*
dbent
.
PaymentProviderInstance
{
func
pcApplyVisibleMethodRouting
(
typeInstances
map
[
string
][]
*
dbent
.
PaymentProviderInstance
,
vals
map
[
string
]
string
,
available
map
[
string
]
bool
)
map
[
string
][]
*
dbent
.
PaymentProviderInstance
{
if
len
(
typeInstances
)
==
0
{
if
len
(
typeInstances
)
==
0
{
return
typeInstances
return
typeInstances
...
...
backend/internal/service/payment_config_limits_test.go
View file @
b22d00e5
...
@@ -301,7 +301,7 @@ func TestPcInstanceTypeLimits(t *testing.T) {
...
@@ -301,7 +301,7 @@ func TestPcInstanceTypeLimits(t *testing.T) {
})
})
}
}
func
TestGetAvailableMethodLimits
Respects
VisibleMethod
Routing
(
t
*
testing
.
T
)
{
func
TestGetAvailableMethodLimits
HidesConflicting
VisibleMethod
Providers
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
client
:=
newPaymentConfigServiceTestClient
(
t
)
client
:=
newPaymentConfigServiceTestClient
(
t
)
...
@@ -341,14 +341,6 @@ func TestGetAvailableMethodLimitsRespectsVisibleMethodRouting(t *testing.T) {
...
@@ -341,14 +341,6 @@ func TestGetAvailableMethodLimitsRespectsVisibleMethodRouting(t *testing.T) {
svc
:=
&
PaymentConfigService
{
svc
:=
&
PaymentConfigService
{
entClient
:
client
,
entClient
:
client
,
settingRepo
:
&
paymentConfigSettingRepoStub
{
values
:
map
[
string
]
string
{
SettingPaymentVisibleMethodAlipayEnabled
:
"true"
,
SettingPaymentVisibleMethodAlipaySource
:
VisibleMethodSourceEasyPayAlipay
,
SettingPaymentVisibleMethodWxpayEnabled
:
"false"
,
SettingPaymentVisibleMethodWxpaySource
:
VisibleMethodSourceOfficialWechat
,
},
},
}
}
resp
,
err
:=
svc
.
GetAvailableMethodLimits
(
ctx
)
resp
,
err
:=
svc
.
GetAvailableMethodLimits
(
ctx
)
...
@@ -356,17 +348,18 @@ func TestGetAvailableMethodLimitsRespectsVisibleMethodRouting(t *testing.T) {
...
@@ -356,17 +348,18 @@ func TestGetAvailableMethodLimitsRespectsVisibleMethodRouting(t *testing.T) {
t
.
Fatalf
(
"GetAvailableMethodLimits returned error: %v"
,
err
)
t
.
Fatalf
(
"GetAvailableMethodLimits returned error: %v"
,
err
)
}
}
alipayLimits
,
ok
:=
resp
.
Methods
[
payment
.
TypeAlipay
]
if
_
,
ok
:=
resp
.
Methods
[
payment
.
TypeAlipay
];
ok
{
if
!
ok
{
t
.
Fatalf
(
"alipay should be hidden when multiple enabled providers claim it, got %v"
,
resp
.
Methods
[
payment
.
TypeAlipay
])
t
.
Fatalf
(
"expected visible alipay limits, got %v"
,
resp
.
Methods
)
}
}
if
alipayLimits
.
SingleMin
!=
20
||
alipayLimits
.
SingleMax
!=
200
{
t
.
Fatalf
(
"alipay limits = %+v, want easypay-only min=20 max=200"
,
alipayLimits
)
wxpayLimits
,
ok
:=
resp
.
Methods
[
payment
.
TypeWxpay
]
if
!
ok
{
t
.
Fatalf
(
"expected wxpay limits to remain visible, got %v"
,
resp
.
Methods
)
}
}
if
_
,
ok
:=
resp
.
Methods
[
payment
.
TypeWxpay
];
ok
{
if
wxpayLimits
.
SingleMin
!=
30
||
wxpayLimits
.
SingleMax
!=
300
{
t
.
Fatalf
(
"wxpay
should be hidden when visible method is disabled, got %v"
,
resp
.
Methods
[
payment
.
TypeWxpay
]
)
t
.
Fatalf
(
"wxpay
limits = %+v, want official-only min=30 max=300"
,
wxpayLimits
)
}
}
if
resp
.
GlobalMin
!=
2
0
||
resp
.
GlobalMax
!=
2
00
{
if
resp
.
GlobalMin
!=
3
0
||
resp
.
GlobalMax
!=
3
00
{
t
.
Fatalf
(
"global range = (%v, %v), want (
2
0,
2
00)"
,
resp
.
GlobalMin
,
resp
.
GlobalMax
)
t
.
Fatalf
(
"global range = (%v, %v), want (
3
0,
3
00)"
,
resp
.
GlobalMin
,
resp
.
GlobalMax
)
}
}
}
}
backend/internal/service/payment_config_providers.go
View file @
b22d00e5
...
@@ -108,6 +108,9 @@ func (s *PaymentConfigService) CreateProviderInstance(ctx context.Context, req C
...
@@ -108,6 +108,9 @@ func (s *PaymentConfigService) CreateProviderInstance(ctx context.Context, req C
if
err
:=
validateProviderRequest
(
req
.
ProviderKey
,
req
.
Name
,
typesStr
);
err
!=
nil
{
if
err
:=
validateProviderRequest
(
req
.
ProviderKey
,
req
.
Name
,
typesStr
);
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
if
err
:=
s
.
validateVisibleMethodEnablementConflicts
(
ctx
,
0
,
req
.
ProviderKey
,
typesStr
,
req
.
Enabled
);
err
!=
nil
{
return
nil
,
err
}
enc
,
err
:=
s
.
encryptConfig
(
req
.
Config
)
enc
,
err
:=
s
.
encryptConfig
(
req
.
Config
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
...
@@ -136,6 +139,21 @@ func validateProviderRequest(providerKey, name, supportedTypes string) error {
...
@@ -136,6 +139,21 @@ func validateProviderRequest(providerKey, name, supportedTypes string) error {
// NOTE: This function exceeds 30 lines due to per-field nil-check patch update
// NOTE: This function exceeds 30 lines due to per-field nil-check patch update
// boilerplate and pending-order safety checks.
// boilerplate and pending-order safety checks.
func
(
s
*
PaymentConfigService
)
UpdateProviderInstance
(
ctx
context
.
Context
,
id
int64
,
req
UpdateProviderInstanceRequest
)
(
*
dbent
.
PaymentProviderInstance
,
error
)
{
func
(
s
*
PaymentConfigService
)
UpdateProviderInstance
(
ctx
context
.
Context
,
id
int64
,
req
UpdateProviderInstanceRequest
)
(
*
dbent
.
PaymentProviderInstance
,
error
)
{
current
,
err
:=
s
.
entClient
.
PaymentProviderInstance
.
Get
(
ctx
,
id
)
if
err
!=
nil
{
return
nil
,
err
}
nextEnabled
:=
current
.
Enabled
if
req
.
Enabled
!=
nil
{
nextEnabled
=
*
req
.
Enabled
}
nextSupportedTypes
:=
current
.
SupportedTypes
if
req
.
SupportedTypes
!=
nil
{
nextSupportedTypes
=
joinTypes
(
req
.
SupportedTypes
)
}
if
err
:=
s
.
validateVisibleMethodEnablementConflicts
(
ctx
,
id
,
current
.
ProviderKey
,
nextSupportedTypes
,
nextEnabled
);
err
!=
nil
{
return
nil
,
err
}
if
req
.
Config
!=
nil
{
if
req
.
Config
!=
nil
{
hasSensitive
:=
false
hasSensitive
:=
false
for
k
:=
range
req
.
Config
{
for
k
:=
range
req
.
Config
{
...
@@ -188,11 +206,7 @@ func (s *PaymentConfigService) UpdateProviderInstance(ctx context.Context, id in
...
@@ -188,11 +206,7 @@ func (s *PaymentConfigService) UpdateProviderInstance(ctx context.Context, id in
}
}
if
count
>
0
{
if
count
>
0
{
// Load current instance to compare types
// Load current instance to compare types
inst
,
err
:=
s
.
entClient
.
PaymentProviderInstance
.
Get
(
ctx
,
id
)
oldTypes
:=
strings
.
Split
(
current
.
SupportedTypes
,
","
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"load provider instance: %w"
,
err
)
}
oldTypes
:=
strings
.
Split
(
inst
.
SupportedTypes
,
","
)
newTypes
:=
req
.
SupportedTypes
newTypes
:=
req
.
SupportedTypes
for
_
,
ot
:=
range
oldTypes
{
for
_
,
ot
:=
range
oldTypes
{
ot
=
strings
.
TrimSpace
(
ot
)
ot
=
strings
.
TrimSpace
(
ot
)
...
@@ -237,10 +251,7 @@ func (s *PaymentConfigService) UpdateProviderInstance(ctx context.Context, id in
...
@@ -237,10 +251,7 @@ func (s *PaymentConfigService) UpdateProviderInstance(ctx context.Context, id in
if
req
.
RefundEnabled
!=
nil
{
if
req
.
RefundEnabled
!=
nil
{
refundEnabled
=
*
req
.
RefundEnabled
refundEnabled
=
*
req
.
RefundEnabled
}
else
{
}
else
{
inst
,
err
:=
s
.
entClient
.
PaymentProviderInstance
.
Get
(
ctx
,
id
)
refundEnabled
=
current
.
RefundEnabled
if
err
==
nil
{
refundEnabled
=
inst
.
RefundEnabled
}
}
}
if
refundEnabled
{
if
refundEnabled
{
u
.
SetAllowUserRefund
(
true
)
u
.
SetAllowUserRefund
(
true
)
...
...
backend/internal/service/payment_config_providers_test.go
View file @
b22d00e5
...
@@ -3,8 +3,10 @@
...
@@ -3,8 +3,10 @@
package
service
package
service
import
(
import
(
"context"
"testing"
"testing"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/require"
)
)
...
@@ -185,3 +187,104 @@ func TestJoinTypes(t *testing.T) {
...
@@ -185,3 +187,104 @@ func TestJoinTypes(t *testing.T) {
})
})
}
}
}
}
func
TestCreateProviderInstanceRejectsConflictingVisibleMethodEnablement
(
t
*
testing
.
T
)
{
t
.
Parallel
()
ctx
:=
context
.
Background
()
client
:=
newPaymentConfigServiceTestClient
(
t
)
svc
:=
&
PaymentConfigService
{
entClient
:
client
,
encryptionKey
:
[]
byte
(
"0123456789abcdef0123456789abcdef"
),
}
_
,
err
:=
svc
.
CreateProviderInstance
(
ctx
,
CreateProviderInstanceRequest
{
ProviderKey
:
"easypay"
,
Name
:
"EasyPay Alipay"
,
Config
:
map
[
string
]
string
{
"pid"
:
"1001"
},
SupportedTypes
:
[]
string
{
"alipay"
},
Enabled
:
true
,
})
require
.
NoError
(
t
,
err
)
_
,
err
=
svc
.
CreateProviderInstance
(
ctx
,
CreateProviderInstanceRequest
{
ProviderKey
:
"alipay"
,
Name
:
"Official Alipay"
,
Config
:
map
[
string
]
string
{
"appId"
:
"app-1"
},
SupportedTypes
:
[]
string
{
"alipay"
},
Enabled
:
true
,
})
require
.
Error
(
t
,
err
)
require
.
Equal
(
t
,
"PAYMENT_PROVIDER_CONFLICT"
,
infraerrors
.
Reason
(
err
))
}
func
TestUpdateProviderInstanceRejectsEnablingConflictingVisibleMethodProvider
(
t
*
testing
.
T
)
{
t
.
Parallel
()
ctx
:=
context
.
Background
()
client
:=
newPaymentConfigServiceTestClient
(
t
)
svc
:=
&
PaymentConfigService
{
entClient
:
client
,
encryptionKey
:
[]
byte
(
"0123456789abcdef0123456789abcdef"
),
}
existing
,
err
:=
svc
.
CreateProviderInstance
(
ctx
,
CreateProviderInstanceRequest
{
ProviderKey
:
"easypay"
,
Name
:
"EasyPay WeChat"
,
Config
:
map
[
string
]
string
{
"pid"
:
"2001"
},
SupportedTypes
:
[]
string
{
"wxpay"
},
Enabled
:
true
,
})
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
existing
)
candidate
,
err
:=
svc
.
CreateProviderInstance
(
ctx
,
CreateProviderInstanceRequest
{
ProviderKey
:
"wxpay"
,
Name
:
"Official WeChat"
,
Config
:
map
[
string
]
string
{
"appId"
:
"wx-app"
},
SupportedTypes
:
[]
string
{
"wxpay"
},
Enabled
:
false
,
})
require
.
NoError
(
t
,
err
)
_
,
err
=
svc
.
UpdateProviderInstance
(
ctx
,
candidate
.
ID
,
UpdateProviderInstanceRequest
{
Enabled
:
boolPtrValue
(
true
),
})
require
.
Error
(
t
,
err
)
require
.
Equal
(
t
,
"PAYMENT_PROVIDER_CONFLICT"
,
infraerrors
.
Reason
(
err
))
}
func
TestUpdateProviderInstancePersistsEnabledAndSupportedTypes
(
t
*
testing
.
T
)
{
t
.
Parallel
()
ctx
:=
context
.
Background
()
client
:=
newPaymentConfigServiceTestClient
(
t
)
svc
:=
&
PaymentConfigService
{
entClient
:
client
,
encryptionKey
:
[]
byte
(
"0123456789abcdef0123456789abcdef"
),
}
instance
,
err
:=
svc
.
CreateProviderInstance
(
ctx
,
CreateProviderInstanceRequest
{
ProviderKey
:
"easypay"
,
Name
:
"EasyPay"
,
Config
:
map
[
string
]
string
{
"pid"
:
"3001"
},
SupportedTypes
:
[]
string
{
"alipay"
},
Enabled
:
false
,
})
require
.
NoError
(
t
,
err
)
_
,
err
=
svc
.
UpdateProviderInstance
(
ctx
,
instance
.
ID
,
UpdateProviderInstanceRequest
{
Enabled
:
boolPtrValue
(
true
),
SupportedTypes
:
[]
string
{
"alipay"
,
"wxpay"
},
})
require
.
NoError
(
t
,
err
)
saved
,
err
:=
client
.
PaymentProviderInstance
.
Get
(
ctx
,
instance
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
True
(
t
,
saved
.
Enabled
)
require
.
Equal
(
t
,
"alipay,wxpay"
,
saved
.
SupportedTypes
)
}
func
boolPtrValue
(
v
bool
)
*
bool
{
return
&
v
}
backend/internal/service/payment_config_service.go
View file @
b22d00e5
...
@@ -209,17 +209,6 @@ func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentCo
...
@@ -209,17 +209,6 @@ func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentCo
return
nil
,
fmt
.
Errorf
(
"get payment config settings: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"get payment config settings: %w"
,
err
)
}
}
cfg
:=
s
.
parsePaymentConfig
(
vals
)
cfg
:=
s
.
parsePaymentConfig
(
vals
)
if
s
.
entClient
!=
nil
{
instances
,
err
:=
s
.
entClient
.
PaymentProviderInstance
.
Query
()
.
Where
(
paymentproviderinstance
.
EnabledEQ
(
true
))
.
All
(
ctx
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"list enabled provider instances: %w"
,
err
)
}
cfg
.
EnabledTypes
=
applyVisibleMethodRoutingToEnabledTypes
(
cfg
.
EnabledTypes
,
vals
,
buildVisibleMethodSourceAvailability
(
instances
))
}
else
{
cfg
.
EnabledTypes
=
applyVisibleMethodRoutingToEnabledTypes
(
cfg
.
EnabledTypes
,
vals
,
nil
)
}
// Load Stripe publishable key from the first enabled Stripe provider instance
// Load Stripe publishable key from the first enabled Stripe provider instance
cfg
.
StripePublishableKey
=
s
.
getStripePublishableKey
(
ctx
)
cfg
.
StripePublishableKey
=
s
.
getStripePublishableKey
(
ctx
)
return
cfg
,
nil
return
cfg
,
nil
...
@@ -305,25 +294,25 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda
...
@@ -305,25 +294,25 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda
}
}
}
}
m
:=
map
[
string
]
string
{
m
:=
map
[
string
]
string
{
SettingPaymentEnabled
:
formatBoolOrEmpty
(
req
.
Enabled
),
SettingPaymentEnabled
:
formatBoolOrEmpty
(
req
.
Enabled
),
SettingMinRechargeAmount
:
formatPositiveFloat
(
req
.
MinAmount
),
SettingMinRechargeAmount
:
formatPositiveFloat
(
req
.
MinAmount
),
SettingMaxRechargeAmount
:
formatPositiveFloat
(
req
.
MaxAmount
),
SettingMaxRechargeAmount
:
formatPositiveFloat
(
req
.
MaxAmount
),
SettingDailyRechargeLimit
:
formatPositiveFloat
(
req
.
DailyLimit
),
SettingDailyRechargeLimit
:
formatPositiveFloat
(
req
.
DailyLimit
),
SettingOrderTimeoutMinutes
:
formatPositiveInt
(
req
.
OrderTimeoutMin
),
SettingOrderTimeoutMinutes
:
formatPositiveInt
(
req
.
OrderTimeoutMin
),
SettingMaxPendingOrders
:
formatPositiveInt
(
req
.
MaxPendingOrders
),
SettingMaxPendingOrders
:
formatPositiveInt
(
req
.
MaxPendingOrders
),
SettingBalancePayDisabled
:
formatBoolOrEmpty
(
req
.
BalanceDisabled
),
SettingBalancePayDisabled
:
formatBoolOrEmpty
(
req
.
BalanceDisabled
),
SettingBalanceRechargeMult
:
formatPositiveFloat
(
req
.
BalanceRechargeMultiplier
),
SettingBalanceRechargeMult
:
formatPositiveFloat
(
req
.
BalanceRechargeMultiplier
),
SettingRechargeFeeRate
:
formatNonNegativeFloat
(
req
.
RechargeFeeRate
),
SettingRechargeFeeRate
:
formatNonNegativeFloat
(
req
.
RechargeFeeRate
),
SettingLoadBalanceStrategy
:
derefStr
(
req
.
LoadBalanceStrategy
),
SettingLoadBalanceStrategy
:
derefStr
(
req
.
LoadBalanceStrategy
),
SettingProductNamePrefix
:
derefStr
(
req
.
ProductNamePrefix
),
SettingProductNamePrefix
:
derefStr
(
req
.
ProductNamePrefix
),
SettingProductNameSuffix
:
derefStr
(
req
.
ProductNameSuffix
),
SettingProductNameSuffix
:
derefStr
(
req
.
ProductNameSuffix
),
SettingHelpImageURL
:
derefStr
(
req
.
HelpImageURL
),
SettingHelpImageURL
:
derefStr
(
req
.
HelpImageURL
),
SettingHelpText
:
derefStr
(
req
.
HelpText
),
SettingHelpText
:
derefStr
(
req
.
HelpText
),
SettingCancelRateLimitOn
:
formatBoolOrEmpty
(
req
.
CancelRateLimitEnabled
),
SettingCancelRateLimitOn
:
formatBoolOrEmpty
(
req
.
CancelRateLimitEnabled
),
SettingCancelRateLimitMax
:
formatPositiveInt
(
req
.
CancelRateLimitMax
),
SettingCancelRateLimitMax
:
formatPositiveInt
(
req
.
CancelRateLimitMax
),
SettingCancelWindowSize
:
formatPositiveInt
(
req
.
CancelRateLimitWindow
),
SettingCancelWindowSize
:
formatPositiveInt
(
req
.
CancelRateLimitWindow
),
SettingCancelWindowUnit
:
derefStr
(
req
.
CancelRateLimitUnit
),
SettingCancelWindowUnit
:
derefStr
(
req
.
CancelRateLimitUnit
),
SettingCancelWindowMode
:
derefStr
(
req
.
CancelRateLimitMode
),
SettingCancelWindowMode
:
derefStr
(
req
.
CancelRateLimitMode
),
SettingPaymentVisibleMethodAlipaySource
:
derefStr
(
req
.
VisibleMethodAlipaySource
),
SettingPaymentVisibleMethodAlipaySource
:
derefStr
(
req
.
VisibleMethodAlipaySource
),
SettingPaymentVisibleMethodWxpaySource
:
derefStr
(
req
.
VisibleMethodWxpaySource
),
SettingPaymentVisibleMethodWxpaySource
:
derefStr
(
req
.
VisibleMethodWxpaySource
),
SettingPaymentVisibleMethodAlipayEnabled
:
formatBoolOrEmpty
(
req
.
VisibleMethodAlipayEnabled
),
SettingPaymentVisibleMethodAlipayEnabled
:
formatBoolOrEmpty
(
req
.
VisibleMethodAlipayEnabled
),
...
...
backend/internal/service/payment_config_service_test.go
View file @
b22d00e5
...
@@ -3,6 +3,8 @@ package service
...
@@ -3,6 +3,8 @@ package service
import
(
import
(
"context"
"context"
"database/sql"
"database/sql"
"fmt"
"strings"
"testing"
"testing"
dbent
"github.com/Wei-Shaw/sub2api/ent"
dbent
"github.com/Wei-Shaw/sub2api/ent"
...
@@ -302,7 +304,7 @@ func TestBuildVisibleMethodSourceAvailability(t *testing.T) {
...
@@ -302,7 +304,7 @@ func TestBuildVisibleMethodSourceAvailability(t *testing.T) {
}
}
}
}
func
TestGetPaymentConfig
AppliesVisibleMethodRouting
(
t
*
testing
.
T
)
{
func
TestGetPaymentConfig
KeepsStoredEnabledTypes
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
client
:=
newPaymentConfigServiceTestClient
(
t
)
client
:=
newPaymentConfigServiceTestClient
(
t
)
...
@@ -321,11 +323,7 @@ func TestGetPaymentConfigAppliesVisibleMethodRouting(t *testing.T) {
...
@@ -321,11 +323,7 @@ func TestGetPaymentConfigAppliesVisibleMethodRouting(t *testing.T) {
entClient
:
client
,
entClient
:
client
,
settingRepo
:
&
paymentConfigSettingRepoStub
{
settingRepo
:
&
paymentConfigSettingRepoStub
{
values
:
map
[
string
]
string
{
values
:
map
[
string
]
string
{
SettingEnabledPaymentTypes
:
"alipay,wxpay,stripe"
,
SettingEnabledPaymentTypes
:
"alipay,wxpay,stripe"
,
SettingPaymentVisibleMethodAlipayEnabled
:
"true"
,
SettingPaymentVisibleMethodAlipaySource
:
"easypay"
,
SettingPaymentVisibleMethodWxpayEnabled
:
"true"
,
SettingPaymentVisibleMethodWxpaySource
:
"wxpay"
,
},
},
},
},
}
}
...
@@ -335,7 +333,7 @@ func TestGetPaymentConfigAppliesVisibleMethodRouting(t *testing.T) {
...
@@ -335,7 +333,7 @@ func TestGetPaymentConfigAppliesVisibleMethodRouting(t *testing.T) {
t
.
Fatalf
(
"GetPaymentConfig returned error: %v"
,
err
)
t
.
Fatalf
(
"GetPaymentConfig returned error: %v"
,
err
)
}
}
want
:=
[]
string
{
payment
.
TypeAlipay
,
payment
.
TypeStripe
}
want
:=
[]
string
{
payment
.
TypeAlipay
,
payment
.
TypeWxpay
,
payment
.
TypeStripe
}
if
len
(
cfg
.
EnabledTypes
)
!=
len
(
want
)
{
if
len
(
cfg
.
EnabledTypes
)
!=
len
(
want
)
{
t
.
Fatalf
(
"EnabledTypes len = %d, want %d (%v)"
,
len
(
cfg
.
EnabledTypes
),
len
(
want
),
cfg
.
EnabledTypes
)
t
.
Fatalf
(
"EnabledTypes len = %d, want %d (%v)"
,
len
(
cfg
.
EnabledTypes
),
len
(
want
),
cfg
.
EnabledTypes
)
}
}
...
@@ -349,7 +347,11 @@ func TestGetPaymentConfigAppliesVisibleMethodRouting(t *testing.T) {
...
@@ -349,7 +347,11 @@ func TestGetPaymentConfigAppliesVisibleMethodRouting(t *testing.T) {
func
newPaymentConfigServiceTestClient
(
t
*
testing
.
T
)
*
dbent
.
Client
{
func
newPaymentConfigServiceTestClient
(
t
*
testing
.
T
)
*
dbent
.
Client
{
t
.
Helper
()
t
.
Helper
()
db
,
err
:=
sql
.
Open
(
"sqlite"
,
"file:payment_config_service?mode=memory&cache=shared"
)
dbName
:=
fmt
.
Sprintf
(
"file:%s?mode=memory&cache=shared"
,
strings
.
NewReplacer
(
"/"
,
"_"
,
" "
,
"_"
)
.
Replace
(
t
.
Name
()),
)
db
,
err
:=
sql
.
Open
(
"sqlite"
,
dbName
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatalf
(
"open sqlite: %v"
,
err
)
t
.
Fatalf
(
"open sqlite: %v"
,
err
)
}
}
...
...
backend/internal/service/payment_order.go
View file @
b22d00e5
...
@@ -326,20 +326,17 @@ func requestNeedsWeChatJSAPICompatibility(req CreateOrderRequest) bool {
...
@@ -326,20 +326,17 @@ func requestNeedsWeChatJSAPICompatibility(req CreateOrderRequest) bool {
}
}
func
(
s
*
PaymentService
)
usesOfficialWxpayVisibleMethod
(
ctx
context
.
Context
)
bool
{
func
(
s
*
PaymentService
)
usesOfficialWxpayVisibleMethod
(
ctx
context
.
Context
)
bool
{
if
s
==
nil
||
s
.
configService
==
nil
||
s
.
configService
.
settingRepo
==
nil
{
if
s
==
nil
||
s
.
configService
==
nil
{
return
false
return
false
}
}
vals
,
err
:=
s
.
configService
.
settingRepo
.
GetMultiple
(
ctx
,
[]
string
{
inst
,
err
:=
s
.
configService
.
resolveEnabledVisibleMethodInstance
(
ctx
,
payment
.
TypeWxpay
)
SettingPaymentVisibleMethodWxpayEnabled
,
SettingPaymentVisibleMethodWxpaySource
,
})
if
err
!=
nil
{
if
err
!=
nil
{
return
false
return
false
}
}
if
vals
[
SettingPaymentVisibleMethodWxpayEnabled
]
!=
"true"
{
if
inst
==
nil
{
return
false
return
false
}
}
return
NormalizeVisibleMethodSource
(
payment
.
TypeWxpay
,
vals
[
SettingPaymentVisibleMethodWxpaySource
])
==
VisibleMethodSourceOfficialWechat
return
inst
.
ProviderKey
==
payment
.
TypeWxpay
}
}
func
(
s
*
PaymentService
)
invokeProvider
(
ctx
context
.
Context
,
order
*
dbent
.
PaymentOrder
,
req
CreateOrderRequest
,
cfg
*
PaymentConfig
,
limitAmount
float64
,
payAmountStr
string
,
payAmount
float64
,
plan
*
dbent
.
SubscriptionPlan
,
sel
*
payment
.
InstanceSelection
)
(
*
CreateOrderResponse
,
error
)
{
func
(
s
*
PaymentService
)
invokeProvider
(
ctx
context
.
Context
,
order
*
dbent
.
PaymentOrder
,
req
CreateOrderRequest
,
cfg
*
PaymentConfig
,
limitAmount
float64
,
payAmountStr
string
,
payAmount
float64
,
plan
*
dbent
.
SubscriptionPlan
,
sel
*
payment
.
InstanceSelection
)
(
*
CreateOrderResponse
,
error
)
{
...
...
backend/internal/service/payment_order_jsapi_test.go
View file @
b22d00e5
...
@@ -2,115 +2,32 @@ package service
...
@@ -2,115 +2,32 @@ package service
import
(
import
(
"context"
"context"
"encoding/json"
"fmt"
"testing"
"testing"
"github.com/Wei-Shaw/sub2api/internal/payment"
"github.com/Wei-Shaw/sub2api/internal/payment"
)
)
const
jsapiTestEncryptionKey
=
"0123456789abcdef0123456789abcdef"
func
TestUsesOfficialWxpayVisibleMethodDerivesFromEnabledProviderInstance
(
t
*
testing
.
T
)
{
func
TestSelectCreateOrderInstancePrefersJSAPICompatibleWxpayInstance
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
client
:=
newPaymentConfigServiceTestClient
(
t
)
client
:=
newPaymentConfigServiceTestClient
(
t
)
compatibleConfig
:=
mustEncryptJSAPITestConfig
(
t
,
map
[
string
]
string
{
_
,
err
:=
client
.
PaymentProviderInstance
.
Create
()
.
"appId"
:
"wx-merchant-app"
,
"mpAppId"
:
"wx-mp-app"
,
"mchId"
:
"mch-compatible"
,
"privateKey"
:
"private-key"
,
"apiV3Key"
:
jsapiTestEncryptionKey
,
"publicKey"
:
"public-key"
,
"publicKeyId"
:
"key-compatible"
,
"certSerial"
:
"serial-compatible"
,
})
incompatibleConfig
:=
mustEncryptJSAPITestConfig
(
t
,
map
[
string
]
string
{
"appId"
:
"wx-merchant-other"
,
"mpAppId"
:
"wx-mp-other"
,
"mchId"
:
"mch-incompatible"
,
"privateKey"
:
"private-key"
,
"apiV3Key"
:
jsapiTestEncryptionKey
,
"publicKey"
:
"public-key"
,
"publicKeyId"
:
"key-incompatible"
,
"certSerial"
:
"serial-incompatible"
,
})
compatible
,
err
:=
client
.
PaymentProviderInstance
.
Create
()
.
SetProviderKey
(
payment
.
TypeWxpay
)
.
SetProviderKey
(
payment
.
TypeWxpay
)
.
SetName
(
"
wxpay-compatible
"
)
.
SetName
(
"
Official WeChat
"
)
.
SetConfig
(
compatibleConfig
)
.
SetConfig
(
"{}"
)
.
SetSupportedTypes
(
"wxpay"
)
.
SetSupportedTypes
(
"wxpay"
)
.
SetEnabled
(
true
)
.
SetEnabled
(
true
)
.
SetSortOrder
(
1
)
.
SetSortOrder
(
1
)
.
Save
(
ctx
)
Save
(
ctx
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatalf
(
"create compatible wxpay instance: %v"
,
err
)
t
.
Fatalf
(
"create official wxpay instance: %v"
,
err
)
}
_
,
err
=
client
.
PaymentProviderInstance
.
Create
()
.
SetProviderKey
(
payment
.
TypeWxpay
)
.
SetName
(
"wxpay-incompatible"
)
.
SetConfig
(
incompatibleConfig
)
.
SetSupportedTypes
(
"wxpay"
)
.
SetEnabled
(
true
)
.
SetSortOrder
(
2
)
.
Save
(
ctx
)
if
err
!=
nil
{
t
.
Fatalf
(
"create incompatible wxpay instance: %v"
,
err
)
}
}
configService
:=
&
PaymentConfigService
{
entClient
:
client
,
settingRepo
:
&
paymentConfigSettingRepoStub
{
values
:
map
[
string
]
string
{
SettingPaymentVisibleMethodWxpayEnabled
:
"true"
,
SettingPaymentVisibleMethodWxpaySource
:
VisibleMethodSourceOfficialWechat
,
SettingKeyWeChatConnectEnabled
:
"true"
,
SettingKeyWeChatConnectAppID
:
"wx-mp-app"
,
SettingKeyWeChatConnectAppSecret
:
"wechat-secret"
,
SettingKeyWeChatConnectMode
:
"mp"
,
SettingKeyWeChatConnectScopes
:
"snsapi_base"
,
SettingKeyWeChatConnectRedirectURL
:
"https://api.example.com/api/v1/auth/oauth/wechat/callback"
,
SettingKeyWeChatConnectFrontendRedirectURL
:
"/auth/wechat/callback"
,
}},
encryptionKey
:
[]
byte
(
jsapiTestEncryptionKey
),
}
loadBalancer
:=
newVisibleMethodLoadBalancer
(
payment
.
NewDefaultLoadBalancer
(
client
,
[]
byte
(
jsapiTestEncryptionKey
)),
configService
,
)
svc
:=
&
PaymentService
{
svc
:=
&
PaymentService
{
entClient
:
client
,
configService
:
&
PaymentConfigService
{
entClient
:
client
},
loadBalancer
:
loadBalancer
,
configService
:
configService
,
}
}
sel
,
err
:=
svc
.
selectCreateOrderInstance
(
ctx
,
CreateOrderRequest
{
if
!
svc
.
usesOfficialWxpayVisibleMethod
(
ctx
)
{
PaymentType
:
payment
.
TypeWxpay
,
t
.
Fatal
(
"expected official wxpay visible method to be detected from enabled provider instance"
)
OpenID
:
"openid-123"
,
IsWeChatBrowser
:
true
,
},
&
PaymentConfig
{
LoadBalanceStrategy
:
string
(
payment
.
StrategyRoundRobin
)},
12.5
)
if
err
!=
nil
{
t
.
Fatalf
(
"selectCreateOrderInstance returned error: %v"
,
err
)
}
if
sel
==
nil
{
t
.
Fatal
(
"expected selected instance, got nil"
)
}
expectedInstanceID
:=
fmt
.
Sprintf
(
"%d"
,
compatible
.
ID
)
if
sel
.
InstanceID
!=
expectedInstanceID
{
t
.
Fatalf
(
"selected instance id = %q, want %q"
,
sel
.
InstanceID
,
expectedInstanceID
)
}
}
func
mustEncryptJSAPITestConfig
(
t
*
testing
.
T
,
config
map
[
string
]
string
)
string
{
t
.
Helper
()
data
,
err
:=
json
.
Marshal
(
config
)
if
err
!=
nil
{
t
.
Fatalf
(
"marshal config: %v"
,
err
)
}
encrypted
,
err
:=
payment
.
Encrypt
(
string
(
data
),
[]
byte
(
jsapiTestEncryptionKey
))
if
err
!=
nil
{
t
.
Fatalf
(
"encrypt config: %v"
,
err
)
}
}
return
encrypted
}
}
backend/internal/service/payment_resume_service.go
View file @
b22d00e5
...
@@ -40,8 +40,8 @@ const (
...
@@ -40,8 +40,8 @@ const (
paymentResumeNotConfiguredCode
=
"PAYMENT_RESUME_NOT_CONFIGURED"
paymentResumeNotConfiguredCode
=
"PAYMENT_RESUME_NOT_CONFIGURED"
paymentResumeNotConfiguredMessage
=
"payment resume tokens require a configured signing key"
paymentResumeNotConfiguredMessage
=
"payment resume tokens require a configured signing key"
paymentResumeTokenTTL
=
24
*
time
.
Hour
paymentResumeTokenTTL
=
24
*
time
.
Hour
wechatPaymentResumeTokenTTL
=
15
*
time
.
Minute
wechatPaymentResumeTokenTTL
=
15
*
time
.
Minute
)
)
type
ResumeTokenClaims
struct
{
type
ResumeTokenClaims
struct
{
...
@@ -163,7 +163,7 @@ func VisibleMethodProviderKeyForSource(method, source string) (string, bool) {
...
@@ -163,7 +163,7 @@ func VisibleMethodProviderKeyForSource(method, source string) (string, bool) {
}
}
func
newVisibleMethodLoadBalancer
(
inner
payment
.
LoadBalancer
,
configService
*
PaymentConfigService
)
payment
.
LoadBalancer
{
func
newVisibleMethodLoadBalancer
(
inner
payment
.
LoadBalancer
,
configService
*
PaymentConfigService
)
payment
.
LoadBalancer
{
if
inner
==
nil
||
configService
==
nil
||
configService
.
settingRepo
==
nil
{
if
inner
==
nil
||
configService
==
nil
||
configService
.
entClient
==
nil
{
return
inner
return
inner
}
}
return
&
visibleMethodLoadBalancer
{
inner
:
inner
,
configService
:
configService
}
return
&
visibleMethodLoadBalancer
{
inner
:
inner
,
configService
:
configService
}
...
@@ -179,21 +179,14 @@ func (lb *visibleMethodLoadBalancer) SelectInstance(ctx context.Context, provide
...
@@ -179,21 +179,14 @@ func (lb *visibleMethodLoadBalancer) SelectInstance(ctx context.Context, provide
return
lb
.
inner
.
SelectInstance
(
ctx
,
providerKey
,
paymentType
,
strategy
,
orderAmount
)
return
lb
.
inner
.
SelectInstance
(
ctx
,
providerKey
,
paymentType
,
strategy
,
orderAmount
)
}
}
enabledKey
:=
visibleMethodEnabledSettingKey
(
visibleMethod
)
inst
,
err
:=
lb
.
configService
.
resolveEnabledVisibleMethodInstance
(
ctx
,
visibleMethod
)
sourceKey
:=
visibleMethodSourceSettingKey
(
visibleMethod
)
vals
,
err
:=
lb
.
configService
.
settingRepo
.
GetMultiple
(
ctx
,
[]
string
{
enabledKey
,
sourceKey
})
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"load visible method routing for %s: %w"
,
visibleMethod
,
err
)
return
nil
,
err
}
if
vals
[
enabledKey
]
!=
"true"
{
return
nil
,
fmt
.
Errorf
(
"visible payment method %s is disabled"
,
visibleMethod
)
}
}
if
inst
==
nil
{
targetProviderKey
,
ok
:=
VisibleMethodProviderKeyForSource
(
visibleMethod
,
vals
[
sourceKey
])
return
nil
,
fmt
.
Errorf
(
"visible payment method %s has no enabled provider instance"
,
visibleMethod
)
if
!
ok
{
return
nil
,
fmt
.
Errorf
(
"visible payment method %s has no valid source"
,
visibleMethod
)
}
}
return
lb
.
inner
.
SelectInstance
(
ctx
,
target
ProviderKey
,
paymentType
,
strategy
,
orderAmount
)
return
lb
.
inner
.
SelectInstance
(
ctx
,
inst
.
ProviderKey
,
paymentType
,
strategy
,
orderAmount
)
}
}
func
visibleMethodEnabledSettingKey
(
method
string
)
string
{
func
visibleMethodEnabledSettingKey
(
method
string
)
string
{
...
...
backend/internal/service/payment_resume_service_test.go
View file @
b22d00e5
...
@@ -344,21 +344,30 @@ func TestVisibleMethodProviderKeyForSource(t *testing.T) {
...
@@ -344,21 +344,30 @@ func TestVisibleMethodProviderKeyForSource(t *testing.T) {
}
}
}
}
func
TestVisibleMethodLoadBalancerUses
ConfiguredSour
ce
(
t
*
testing
.
T
)
{
func
TestVisibleMethodLoadBalancerUses
EnabledProviderInstan
ce
(
t
*
testing
.
T
)
{
t
.
Parallel
()
t
.
Parallel
()
ctx
:=
context
.
Background
()
client
:=
newPaymentConfigServiceTestClient
(
t
)
_
,
err
:=
client
.
PaymentProviderInstance
.
Create
()
.
SetProviderKey
(
payment
.
TypeAlipay
)
.
SetName
(
"Official Alipay"
)
.
SetConfig
(
"{}"
)
.
SetSupportedTypes
(
"alipay"
)
.
SetEnabled
(
true
)
.
SetSortOrder
(
1
)
.
Save
(
ctx
)
if
err
!=
nil
{
t
.
Fatalf
(
"create alipay provider: %v"
,
err
)
}
inner
:=
&
captureLoadBalancer
{}
inner
:=
&
captureLoadBalancer
{}
configService
:=
&
PaymentConfigService
{
configService
:=
&
PaymentConfigService
{
settingRepo
:
&
paymentSettingRepoStub
{
entClient
:
client
,
values
:
map
[
string
]
string
{
SettingPaymentVisibleMethodAlipayEnabled
:
"true"
,
SettingPaymentVisibleMethodAlipaySource
:
VisibleMethodSourceOfficialAlipay
,
},
},
}
}
lb
:=
newVisibleMethodLoadBalancer
(
inner
,
configService
)
lb
:=
newVisibleMethodLoadBalancer
(
inner
,
configService
)
_
,
err
:
=
lb
.
SelectInstance
(
c
ontext
.
Background
()
,
""
,
payment
.
TypeAlipay
,
payment
.
StrategyRoundRobin
,
12.5
)
_
,
err
=
lb
.
SelectInstance
(
c
tx
,
""
,
payment
.
TypeAlipay
,
payment
.
StrategyRoundRobin
,
12.5
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatalf
(
"SelectInstance returned error: %v"
,
err
)
t
.
Fatalf
(
"SelectInstance returned error: %v"
,
err
)
}
}
...
@@ -367,46 +376,19 @@ func TestVisibleMethodLoadBalancerUsesConfiguredSource(t *testing.T) {
...
@@ -367,46 +376,19 @@ func TestVisibleMethodLoadBalancerUsesConfiguredSource(t *testing.T) {
}
}
}
}
func
TestVisibleMethodLoadBalancerRejects
D
isabledVisibleMethod
(
t
*
testing
.
T
)
{
func
TestVisibleMethodLoadBalancerRejects
M
is
singEn
abledVisibleMethod
Provider
(
t
*
testing
.
T
)
{
t
.
Parallel
()
t
.
Parallel
()
inner
:=
&
captureLoadBalancer
{}
inner
:=
&
captureLoadBalancer
{}
configService
:=
&
PaymentConfigService
{
configService
:=
&
PaymentConfigService
{
settingRepo
:
&
paymentSettingRepoStub
{
entClient
:
newPaymentConfigServiceTestClient
(
t
),
values
:
map
[
string
]
string
{
SettingPaymentVisibleMethodWxpayEnabled
:
"false"
,
SettingPaymentVisibleMethodWxpaySource
:
VisibleMethodSourceOfficialWechat
,
},
},
}
}
lb
:=
newVisibleMethodLoadBalancer
(
inner
,
configService
)
lb
:=
newVisibleMethodLoadBalancer
(
inner
,
configService
)
if
_
,
err
:=
lb
.
SelectInstance
(
context
.
Background
(),
""
,
payment
.
TypeWxpay
,
payment
.
StrategyRoundRobin
,
9.9
);
err
==
nil
{
if
_
,
err
:=
lb
.
SelectInstance
(
context
.
Background
(),
""
,
payment
.
TypeWxpay
,
payment
.
StrategyRoundRobin
,
9.9
);
err
==
nil
{
t
.
Fatal
(
"SelectInstance should reject disabled visible method"
)
t
.
Fatal
(
"SelectInstance should reject when no enabled provider instance exists"
)
}
}
type
paymentSettingRepoStub
struct
{
values
map
[
string
]
string
}
func
(
s
*
paymentSettingRepoStub
)
Get
(
context
.
Context
,
string
)
(
*
Setting
,
error
)
{
return
nil
,
nil
}
func
(
s
*
paymentSettingRepoStub
)
GetValue
(
_
context
.
Context
,
key
string
)
(
string
,
error
)
{
return
s
.
values
[
key
],
nil
}
func
(
s
*
paymentSettingRepoStub
)
Set
(
context
.
Context
,
string
,
string
)
error
{
return
nil
}
func
(
s
*
paymentSettingRepoStub
)
GetMultiple
(
_
context
.
Context
,
keys
[]
string
)
(
map
[
string
]
string
,
error
)
{
out
:=
make
(
map
[
string
]
string
,
len
(
keys
))
for
_
,
key
:=
range
keys
{
out
[
key
]
=
s
.
values
[
key
]
}
}
return
out
,
nil
}
func
(
s
*
paymentSettingRepoStub
)
SetMultiple
(
context
.
Context
,
map
[
string
]
string
)
error
{
return
nil
}
func
(
s
*
paymentSettingRepoStub
)
GetAll
(
context
.
Context
)
(
map
[
string
]
string
,
error
)
{
return
s
.
values
,
nil
}
}
func
(
s
*
paymentSettingRepoStub
)
Delete
(
context
.
Context
,
string
)
error
{
return
nil
}
type
captureLoadBalancer
struct
{
type
captureLoadBalancer
struct
{
lastProviderKey
string
lastProviderKey
string
...
...
backend/internal/service/payment_visible_method_instances.go
0 → 100644
View file @
b22d00e5
package
service
import
(
"context"
"fmt"
"strings"
dbent
"github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
"github.com/Wei-Shaw/sub2api/internal/payment"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
)
func
enabledVisibleMethodsForProvider
(
providerKey
,
supportedTypes
string
)
[]
string
{
methodSet
:=
make
(
map
[
string
]
struct
{},
2
)
addMethod
:=
func
(
method
string
)
{
method
=
NormalizeVisibleMethod
(
method
)
switch
method
{
case
payment
.
TypeAlipay
,
payment
.
TypeWxpay
:
methodSet
[
method
]
=
struct
{}{}
}
}
switch
strings
.
TrimSpace
(
providerKey
)
{
case
payment
.
TypeAlipay
:
if
strings
.
TrimSpace
(
supportedTypes
)
==
""
{
addMethod
(
payment
.
TypeAlipay
)
break
}
for
_
,
supportedType
:=
range
splitTypes
(
supportedTypes
)
{
if
NormalizeVisibleMethod
(
supportedType
)
==
payment
.
TypeAlipay
{
addMethod
(
payment
.
TypeAlipay
)
break
}
}
case
payment
.
TypeWxpay
:
if
strings
.
TrimSpace
(
supportedTypes
)
==
""
{
addMethod
(
payment
.
TypeWxpay
)
break
}
for
_
,
supportedType
:=
range
splitTypes
(
supportedTypes
)
{
if
NormalizeVisibleMethod
(
supportedType
)
==
payment
.
TypeWxpay
{
addMethod
(
payment
.
TypeWxpay
)
break
}
}
case
payment
.
TypeEasyPay
:
for
_
,
supportedType
:=
range
splitTypes
(
supportedTypes
)
{
addMethod
(
supportedType
)
}
}
methods
:=
make
([]
string
,
0
,
len
(
methodSet
))
for
_
,
method
:=
range
[]
string
{
payment
.
TypeAlipay
,
payment
.
TypeWxpay
}
{
if
_
,
ok
:=
methodSet
[
method
];
ok
{
methods
=
append
(
methods
,
method
)
}
}
return
methods
}
func
providerSupportsVisibleMethod
(
inst
*
dbent
.
PaymentProviderInstance
,
method
string
)
bool
{
if
inst
==
nil
||
!
inst
.
Enabled
{
return
false
}
method
=
NormalizeVisibleMethod
(
method
)
for
_
,
candidate
:=
range
enabledVisibleMethodsForProvider
(
inst
.
ProviderKey
,
inst
.
SupportedTypes
)
{
if
candidate
==
method
{
return
true
}
}
return
false
}
func
filterEnabledVisibleMethodInstances
(
instances
[]
*
dbent
.
PaymentProviderInstance
,
method
string
)
[]
*
dbent
.
PaymentProviderInstance
{
filtered
:=
make
([]
*
dbent
.
PaymentProviderInstance
,
0
,
len
(
instances
))
for
_
,
inst
:=
range
instances
{
if
providerSupportsVisibleMethod
(
inst
,
method
)
{
filtered
=
append
(
filtered
,
inst
)
}
}
return
filtered
}
func
buildPaymentProviderConflictError
(
method
string
,
conflicting
*
dbent
.
PaymentProviderInstance
)
error
{
metadata
:=
map
[
string
]
string
{
"payment_method"
:
NormalizeVisibleMethod
(
method
),
}
if
conflicting
!=
nil
{
metadata
[
"conflicting_provider_id"
]
=
fmt
.
Sprintf
(
"%d"
,
conflicting
.
ID
)
metadata
[
"conflicting_provider_key"
]
=
conflicting
.
ProviderKey
metadata
[
"conflicting_provider_name"
]
=
conflicting
.
Name
}
return
infraerrors
.
Conflict
(
"PAYMENT_PROVIDER_CONFLICT"
,
fmt
.
Sprintf
(
"%s payment already has an enabled provider instance"
,
NormalizeVisibleMethod
(
method
)),
)
.
WithMetadata
(
metadata
)
}
func
(
s
*
PaymentConfigService
)
validateVisibleMethodEnablementConflicts
(
ctx
context
.
Context
,
excludeID
int64
,
providerKey
string
,
supportedTypes
string
,
enabled
bool
,
)
error
{
if
s
==
nil
||
s
.
entClient
==
nil
||
!
enabled
{
return
nil
}
claimedMethods
:=
enabledVisibleMethodsForProvider
(
providerKey
,
supportedTypes
)
if
len
(
claimedMethods
)
==
0
{
return
nil
}
query
:=
s
.
entClient
.
PaymentProviderInstance
.
Query
()
.
Where
(
paymentproviderinstance
.
EnabledEQ
(
true
))
if
excludeID
>
0
{
query
=
query
.
Where
(
paymentproviderinstance
.
IDNEQ
(
excludeID
))
}
instances
,
err
:=
query
.
All
(
ctx
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"query enabled payment providers: %w"
,
err
)
}
for
_
,
method
:=
range
claimedMethods
{
for
_
,
inst
:=
range
instances
{
if
providerSupportsVisibleMethod
(
inst
,
method
)
{
return
buildPaymentProviderConflictError
(
method
,
inst
)
}
}
}
return
nil
}
func
(
s
*
PaymentConfigService
)
resolveEnabledVisibleMethodInstance
(
ctx
context
.
Context
,
method
string
,
)
(
*
dbent
.
PaymentProviderInstance
,
error
)
{
if
s
==
nil
||
s
.
entClient
==
nil
{
return
nil
,
nil
}
method
=
NormalizeVisibleMethod
(
method
)
if
method
!=
payment
.
TypeAlipay
&&
method
!=
payment
.
TypeWxpay
{
return
nil
,
nil
}
instances
,
err
:=
s
.
entClient
.
PaymentProviderInstance
.
Query
()
.
Where
(
paymentproviderinstance
.
EnabledEQ
(
true
))
.
Order
(
paymentproviderinstance
.
BySortOrder
())
.
All
(
ctx
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"query enabled payment providers: %w"
,
err
)
}
matching
:=
filterEnabledVisibleMethodInstances
(
instances
,
method
)
switch
len
(
matching
)
{
case
0
:
return
nil
,
nil
case
1
:
return
matching
[
0
],
nil
default
:
return
nil
,
buildPaymentProviderConflictError
(
method
,
matching
[
0
])
}
}
frontend/src/i18n/locales/en.ts
View file @
b22d00e5
...
@@ -4757,6 +4757,7 @@ export default {
...
@@ -4757,6 +4757,7 @@ export default {
supportedTypesHint
:
'
Comma-separated, e.g. alipay,wxpay
'
,
supportedTypesHint
:
'
Comma-separated, e.g. alipay,wxpay
'
,
refundEnabled
:
'
Allow Refund
'
,
refundEnabled
:
'
Allow Refund
'
,
allowUserRefund
:
'
Allow User Refund
'
,
allowUserRefund
:
'
Allow User Refund
'
,
enableConflict
:
'
{method} already has an enabled provider instance: {provider}. Disable the existing instance before switching.
'
,
},
},
balanceNotify
:
{
balanceNotify
:
{
title
:
'
Balance Low Notification
'
,
title
:
'
Balance Low Notification
'
,
...
@@ -5612,6 +5613,7 @@ export default {
...
@@ -5612,6 +5613,7 @@ export default {
alipayMobileUnavailable
:
'
This page could not hand off to Alipay.
'
,
alipayMobileUnavailable
:
'
This page could not hand off to Alipay.
'
,
alipayMobileOpenHint
:
'
Allow the current page to open the Alipay app, or retry from the system browser.
'
,
alipayMobileOpenHint
:
'
Allow the current page to open the Alipay app, or retry from the system browser.
'
,
PENDING_ORDERS
:
'
This provider has pending orders. Please wait for them to complete before making changes.
'
,
PENDING_ORDERS
:
'
This provider has pending orders. Please wait for them to complete before making changes.
'
,
PAYMENT_PROVIDER_CONFLICT
:
'
Another enabled provider instance is already serving this payment method. Disable it before continuing.
'
,
},
},
stripePay
:
'
Pay Now
'
,
stripePay
:
'
Pay Now
'
,
stripeSuccessProcessing
:
'
Payment successful, processing your order...
'
,
stripeSuccessProcessing
:
'
Payment successful, processing your order...
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
b22d00e5
...
@@ -4921,6 +4921,7 @@ export default {
...
@@ -4921,6 +4921,7 @@ export default {
supportedTypesHint
:
'
逗号分隔,如 alipay,wxpay
'
,
supportedTypesHint
:
'
逗号分隔,如 alipay,wxpay
'
,
refundEnabled
:
'
允许退款
'
,
refundEnabled
:
'
允许退款
'
,
allowUserRefund
:
'
允许用户退款
'
,
allowUserRefund
:
'
允许用户退款
'
,
enableConflict
:
'
{method} 已有启用中的服务商实例:{provider}。请先停用现有实例后再启用或切换。
'
,
},
},
balanceNotify
:
{
balanceNotify
:
{
title
:
'
余额不足提醒
'
,
title
:
'
余额不足提醒
'
,
...
@@ -5800,6 +5801,7 @@ export default {
...
@@ -5800,6 +5801,7 @@ export default {
alipayMobileUnavailable
:
'
当前页面未成功跳转到支付宝。
'
,
alipayMobileUnavailable
:
'
当前页面未成功跳转到支付宝。
'
,
alipayMobileOpenHint
:
'
请允许当前页面打开支付宝 App,或改用系统浏览器重新发起支付。
'
,
alipayMobileOpenHint
:
'
请允许当前页面打开支付宝 App,或改用系统浏览器重新发起支付。
'
,
PENDING_ORDERS
:
'
该服务商有未完成的订单,请等待订单完成后再操作
'
,
PENDING_ORDERS
:
'
该服务商有未完成的订单,请等待订单完成后再操作
'
,
PAYMENT_PROVIDER_CONFLICT
:
'
该支付方式已有其他启用中的服务商实例,请先停用后再继续。
'
,
},
},
stripePay
:
'
立即支付
'
,
stripePay
:
'
立即支付
'
,
stripeSuccessProcessing
:
'
支付成功,正在处理订单...
'
,
stripeSuccessProcessing
:
'
支付成功,正在处理订单...
'
,
...
...
frontend/src/views/admin/SettingsView.vue
View file @
b22d00e5
...
@@ -4160,73 +4160,6 @@
...
@@ -4160,73 +4160,6 @@
<
/a
>
<
/a
>
<
/p
>
<
/p
>
<
/div
>
<
/div
>
<
div
class
=
"
grid grid-cols-1 gap-3 lg:grid-cols-2
"
>
<
div
v
-
for
=
"
visibleMethod in paymentVisibleMethodCards
"
:
key
=
"
visibleMethod.key
"
class
=
"
rounded-lg border border-gray-200 p-4 dark:border-dark-700
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
>
<
label
class
=
"
font-medium text-gray-900 dark:text-white
"
>
{{
t
(
"
admin.settings.paymentVisibleMethods.methodLabel
"
,
{
title
:
visibleMethod
.
title
,
}
)
}}
<
/label
>
<
p
class
=
"
mt-1 text-sm text-gray-500 dark:text-gray-400
"
>
{{
t
(
"
admin.settings.paymentVisibleMethods.methodHint
"
)
}}
<
/p
>
<
/div
>
<
Toggle
:
model
-
value
=
"
getPaymentVisibleMethodEnabled(visibleMethod.key)
"
@
update
:
model
-
value
=
"
setPaymentVisibleMethodEnabled(
visibleMethod.key,
$event,
)
"
/>
<
/div
>
<
div
class
=
"
mt-4
"
>
<
label
class
=
"
input-label
"
>
{{
t
(
"
admin.settings.paymentVisibleMethods.sourceLabel
"
)
}}
<
/label
>
<
Select
:
model
-
value
=
"
getPaymentVisibleMethodSource(visibleMethod.key)
"
:
options
=
"
getPaymentVisibleMethodSourceSelectOptions(
visibleMethod.key,
)
"
@
update
:
model
-
value
=
"
setPaymentVisibleMethodSource(
visibleMethod.key,
$event,
)
"
:
placeholder
=
"
visibleMethod.key
"
/>
<
p
class
=
"
mt-1.5 text-xs text-gray-400
"
>
{{
t
(
"
admin.settings.paymentVisibleMethods.sourceHint
"
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/div
>
<!--
Row
5
:
Help
image
+
text
-->
<!--
Row
5
:
Help
image
+
text
-->
<
div
class
=
"
grid grid-cols-2 gap-3
"
>
<
div
class
=
"
grid grid-cols-2 gap-3
"
>
<
div
>
<
div
>
...
@@ -4742,15 +4675,12 @@ import {
...
@@ -4742,15 +4675,12 @@ import {
buildAuthSourceDefaultsState
,
buildAuthSourceDefaultsState
,
defaultWeChatConnectScopesForMode
,
defaultWeChatConnectScopesForMode
,
deriveWeChatConnectStoredMode
,
deriveWeChatConnectStoredMode
,
getPaymentVisibleMethodSourceOptions
,
normalizePaymentVisibleMethodSource
,
normalizeDefaultSubscriptionSettings
,
normalizeDefaultSubscriptionSettings
,
resolveWeChatConnectModeCapabilities
,
resolveWeChatConnectModeCapabilities
,
}
from
"
@/api/admin/settings
"
;
}
from
"
@/api/admin/settings
"
;
import
type
{
import
type
{
AuthSourceDefaultsState
,
AuthSourceDefaultsState
,
AuthSourceType
,
AuthSourceType
,
PaymentVisibleMethod
,
SystemSettings
,
SystemSettings
,
UpdateSettingsRequest
,
UpdateSettingsRequest
,
DefaultSubscriptionSetting
,
DefaultSubscriptionSetting
,
...
@@ -4777,6 +4707,7 @@ import { useClipboard } from "@/composables/useClipboard";
...
@@ -4777,6 +4707,7 @@ import { useClipboard } from "@/composables/useClipboard";
import
{
extractApiErrorMessage
}
from
"
@/utils/apiError
"
;
import
{
extractApiErrorMessage
}
from
"
@/utils/apiError
"
;
import
{
useAppStore
}
from
"
@/stores
"
;
import
{
useAppStore
}
from
"
@/stores
"
;
import
{
useAdminSettingsStore
}
from
"
@/stores/adminSettings
"
;
import
{
useAdminSettingsStore
}
from
"
@/stores/adminSettings
"
;
import
{
normalizeVisibleMethod
}
from
"
@/components/payment/paymentFlow
"
;
import
{
import
{
isRegistrationEmailSuffixDomainValid
,
isRegistrationEmailSuffixDomainValid
,
normalizeRegistrationEmailSuffixDomain
,
normalizeRegistrationEmailSuffixDomain
,
...
@@ -4788,10 +4719,6 @@ const { t, locale } = useI18n();
...
@@ -4788,10 +4719,6 @@ const { t, locale } = useI18n();
const
appStore
=
useAppStore
();
const
appStore
=
useAppStore
();
const
adminSettingsStore
=
useAdminSettingsStore
();
const
adminSettingsStore
=
useAdminSettingsStore
();
function
localText
(
zh
:
string
,
en
:
string
):
string
{
return
locale
.
value
.
startsWith
(
"
zh
"
)
?
zh
:
en
;
}
type
SettingsTab
=
type
SettingsTab
=
|
"
general
"
|
"
general
"
|
"
security
"
|
"
security
"
...
@@ -4908,10 +4835,6 @@ type SettingsForm = Omit<
...
@@ -4908,10 +4835,6 @@ type SettingsForm = Omit<
wechat_connect_mobile_enabled
:
boolean
;
wechat_connect_mobile_enabled
:
boolean
;
oidc_connect_client_secret
:
string
;
oidc_connect_client_secret
:
string
;
force_email_on_third_party_signup
:
boolean
;
force_email_on_third_party_signup
:
boolean
;
payment_visible_method_alipay_source
:
string
;
payment_visible_method_wxpay_source
:
string
;
payment_visible_method_alipay_enabled
:
boolean
;
payment_visible_method_wxpay_enabled
:
boolean
;
openai_advanced_scheduler_enabled
:
boolean
;
openai_advanced_scheduler_enabled
:
boolean
;
}
;
}
;
...
@@ -4957,10 +4880,6 @@ const form = reactive<SettingsForm>({
...
@@ -4957,10 +4880,6 @@ const form = reactive<SettingsForm>({
payment_cancel_rate_limit_window
:
1
,
payment_cancel_rate_limit_window
:
1
,
payment_cancel_rate_limit_unit
:
"
day
"
,
payment_cancel_rate_limit_unit
:
"
day
"
,
payment_cancel_rate_limit_window_mode
:
"
rolling
"
,
payment_cancel_rate_limit_window_mode
:
"
rolling
"
,
payment_visible_method_alipay_source
:
""
,
payment_visible_method_wxpay_source
:
""
,
payment_visible_method_alipay_enabled
:
false
,
payment_visible_method_wxpay_enabled
:
false
,
table_default_page_size
:
tablePageSizeDefault
,
table_default_page_size
:
tablePageSizeDefault
,
table_page_size_options
:
[
10
,
20
,
50
,
100
],
table_page_size_options
:
[
10
,
20
,
50
,
100
],
custom_menu_items
:
[]
as
Array
<
{
custom_menu_items
:
[]
as
Array
<
{
...
@@ -5099,86 +5018,6 @@ const authSourceDefaultsMeta = computed(() => [
...
@@ -5099,86 +5018,6 @@ const authSourceDefaultsMeta = computed(() => [
}
,
}
,
]);
]);
const
paymentVisibleMethodCards
=
computed
(()
=>
[
{
key
:
"
alipay
"
as
const
,
title
:
t
(
"
payment.methods.alipay
"
),
enabledField
:
"
payment_visible_method_alipay_enabled
"
as
const
,
sourceField
:
"
payment_visible_method_alipay_source
"
as
const
,
}
,
{
key
:
"
wxpay
"
as
const
,
title
:
t
(
"
payment.methods.wxpay
"
),
enabledField
:
"
payment_visible_method_wxpay_enabled
"
as
const
,
sourceField
:
"
payment_visible_method_wxpay_source
"
as
const
,
}
,
]);
function
getPaymentVisibleMethodEnabled
(
method
:
"
alipay
"
|
"
wxpay
"
):
boolean
{
return
method
===
"
alipay
"
?
form
.
payment_visible_method_alipay_enabled
:
form
.
payment_visible_method_wxpay_enabled
;
}
function
setPaymentVisibleMethodEnabled
(
method
:
"
alipay
"
|
"
wxpay
"
,
enabled
:
boolean
,
)
{
if
(
method
===
"
alipay
"
)
{
form
.
payment_visible_method_alipay_enabled
=
enabled
;
return
;
}
form
.
payment_visible_method_wxpay_enabled
=
enabled
;
}
function
getPaymentVisibleMethodSource
(
method
:
"
alipay
"
|
"
wxpay
"
):
string
{
return
method
===
"
alipay
"
?
form
.
payment_visible_method_alipay_source
:
form
.
payment_visible_method_wxpay_source
;
}
function
getPaymentVisibleMethodSourceSelectOptions
(
method
:
PaymentVisibleMethod
,
)
{
return
getPaymentVisibleMethodSourceOptions
(
method
).
map
((
option
)
=>
({
value
:
option
.
value
,
label
:
localText
(
option
.
labelZh
,
option
.
labelEn
),
}
));
}
function
setPaymentVisibleMethodSource
(
method
:
"
alipay
"
|
"
wxpay
"
,
source
:
string
|
number
|
boolean
|
null
,
)
{
const
normalized
=
normalizePaymentVisibleMethodSource
(
method
,
source
);
if
(
method
===
"
alipay
"
)
{
form
.
payment_visible_method_alipay_source
=
normalized
;
return
;
}
form
.
payment_visible_method_wxpay_source
=
normalized
;
}
function
validatePaymentVisibleMethodSelections
():
boolean
{
for
(
const
visibleMethod
of
paymentVisibleMethodCards
.
value
)
{
if
(
!
getPaymentVisibleMethodEnabled
(
visibleMethod
.
key
))
{
continue
;
}
if
(
getPaymentVisibleMethodSource
(
visibleMethod
.
key
))
{
continue
;
}
appStore
.
showError
(
t
(
"
admin.settings.paymentVisibleMethods.sourceRequiredError
"
,
{
title
:
visibleMethod
.
title
,
}
),
);
return
false
;
}
return
true
;
}
// Proxies for web search emulation ProxySelector
// Proxies for web search emulation ProxySelector
const
webSearchProxies
=
ref
<
Proxy
[]
>
([]);
const
webSearchProxies
=
ref
<
Proxy
[]
>
([]);
...
@@ -5660,16 +5499,6 @@ async function loadSettings() {
...
@@ -5660,16 +5499,6 @@ async function loadSettings() {
form
.
default_subscriptions
=
normalizeDefaultSubscriptionSettings
(
form
.
default_subscriptions
=
normalizeDefaultSubscriptionSettings
(
settings
.
default_subscriptions
,
settings
.
default_subscriptions
,
);
);
form
.
payment_visible_method_alipay_source
=
normalizePaymentVisibleMethodSource
(
"
alipay
"
,
settings
.
payment_visible_method_alipay_source
,
);
form
.
payment_visible_method_wxpay_source
=
normalizePaymentVisibleMethodSource
(
"
wxpay
"
,
settings
.
payment_visible_method_wxpay_source
,
);
registrationEmailSuffixWhitelistTags
.
value
=
registrationEmailSuffixWhitelistTags
.
value
=
normalizeRegistrationEmailSuffixDomains
(
normalizeRegistrationEmailSuffixDomains
(
settings
.
registration_email_suffix_whitelist
,
settings
.
registration_email_suffix_whitelist
,
...
@@ -5873,7 +5702,6 @@ async function saveSettings() {
...
@@ -5873,7 +5702,6 @@ async function saveSettings() {
);
);
return
;
return
;
}
}
// Validate URL fields — novalidate disables browser-native checks, so we validate here
// Validate URL fields — novalidate disables browser-native checks, so we validate here
const
isValidHttpUrl
=
(
url
:
string
):
boolean
=>
{
const
isValidHttpUrl
=
(
url
:
string
):
boolean
=>
{
if
(
!
url
)
return
true
;
if
(
!
url
)
return
true
;
...
@@ -6028,18 +5856,6 @@ async function saveSettings() {
...
@@ -6028,18 +5856,6 @@ async function saveSettings() {
payment_cancel_rate_limit_unit
:
form
.
payment_cancel_rate_limit_unit
,
payment_cancel_rate_limit_unit
:
form
.
payment_cancel_rate_limit_unit
,
payment_cancel_rate_limit_window_mode
:
payment_cancel_rate_limit_window_mode
:
form
.
payment_cancel_rate_limit_window_mode
,
form
.
payment_cancel_rate_limit_window_mode
,
payment_visible_method_alipay_source
:
normalizePaymentVisibleMethodSource
(
"
alipay
"
,
form
.
payment_visible_method_alipay_source
,
),
payment_visible_method_wxpay_source
:
normalizePaymentVisibleMethodSource
(
"
wxpay
"
,
form
.
payment_visible_method_wxpay_source
,
),
payment_visible_method_alipay_enabled
:
form
.
payment_visible_method_alipay_enabled
,
payment_visible_method_wxpay_enabled
:
form
.
payment_visible_method_wxpay_enabled
,
openai_advanced_scheduler_enabled
:
form
.
openai_advanced_scheduler_enabled
,
openai_advanced_scheduler_enabled
:
form
.
openai_advanced_scheduler_enabled
,
// Balance & quota notification
// Balance & quota notification
balance_low_notify_enabled
:
form
.
balance_low_notify_enabled
,
balance_low_notify_enabled
:
form
.
balance_low_notify_enabled
,
...
@@ -6062,16 +5878,6 @@ async function saveSettings() {
...
@@ -6062,16 +5878,6 @@ async function saveSettings() {
}
}
}
}
Object
.
assign
(
authSourceDefaults
,
buildAuthSourceDefaultsState
(
updated
));
Object
.
assign
(
authSourceDefaults
,
buildAuthSourceDefaultsState
(
updated
));
form
.
payment_visible_method_alipay_source
=
normalizePaymentVisibleMethodSource
(
"
alipay
"
,
updated
.
payment_visible_method_alipay_source
,
);
form
.
payment_visible_method_wxpay_source
=
normalizePaymentVisibleMethodSource
(
"
wxpay
"
,
updated
.
payment_visible_method_wxpay_source
,
);
registrationEmailSuffixWhitelistTags
.
value
=
registrationEmailSuffixWhitelistTags
.
value
=
normalizeRegistrationEmailSuffixDomains
(
normalizeRegistrationEmailSuffixDomains
(
updated
.
registration_email_suffix_whitelist
,
updated
.
registration_email_suffix_whitelist
,
...
@@ -6588,8 +6394,98 @@ const cancelRateLimitModeOptions = computed(() => [
...
@@ -6588,8 +6394,98 @@ const cancelRateLimitModeOptions = computed(() => [
const
paymentErrorMap
=
computed
(()
=>
({
const
paymentErrorMap
=
computed
(()
=>
({
PENDING_ORDERS
:
t
(
"
payment.errors.PENDING_ORDERS
"
),
PENDING_ORDERS
:
t
(
"
payment.errors.PENDING_ORDERS
"
),
PAYMENT_PROVIDER_CONFLICT
:
t
(
"
payment.errors.PAYMENT_PROVIDER_CONFLICT
"
),
}
));
}
));
type
ProviderEnablementCandidate
=
Pick
<
ProviderInstance
,
"
id
"
|
"
provider_key
"
|
"
supported_types
"
|
"
enabled
"
|
"
name
"
>
;
function
getProviderVisibleMethods
(
provider
:
ProviderEnablementCandidate
,
):
Array
<
"
alipay
"
|
"
wxpay
"
>
{
if
(
!
provider
.
enabled
)
{
return
[];
}
const
supportedTypes
=
Array
.
isArray
(
provider
.
supported_types
)
?
provider
.
supported_types
:
[];
const
methods
=
new
Set
<
"
alipay
"
|
"
wxpay
"
>
();
const
addMethod
=
(
type
:
string
)
=>
{
const
method
=
normalizeVisibleMethod
(
type
);
if
(
method
===
"
alipay
"
||
method
===
"
wxpay
"
)
{
methods
.
add
(
method
);
}
}
;
if
(
provider
.
provider_key
===
"
alipay
"
)
{
if
(
supportedTypes
.
length
===
0
)
{
methods
.
add
(
"
alipay
"
);
}
else
{
supportedTypes
.
forEach
((
type
)
=>
{
if
(
normalizeVisibleMethod
(
type
)
===
"
alipay
"
)
{
methods
.
add
(
"
alipay
"
);
}
}
);
}
}
else
if
(
provider
.
provider_key
===
"
wxpay
"
)
{
if
(
supportedTypes
.
length
===
0
)
{
methods
.
add
(
"
wxpay
"
);
}
else
{
supportedTypes
.
forEach
((
type
)
=>
{
if
(
normalizeVisibleMethod
(
type
)
===
"
wxpay
"
)
{
methods
.
add
(
"
wxpay
"
);
}
}
);
}
}
else
if
(
provider
.
provider_key
===
"
easypay
"
)
{
supportedTypes
.
forEach
(
addMethod
);
}
return
Array
.
from
(
methods
);
}
function
findProviderEnablementConflict
(
candidate
:
ProviderEnablementCandidate
,
):
{
method
:
"
alipay
"
|
"
wxpay
"
;
conflicting
:
ProviderInstance
}
|
null
{
const
claimedMethods
=
getProviderVisibleMethods
(
candidate
);
if
(
claimedMethods
.
length
===
0
)
{
return
null
;
}
for
(
const
other
of
providers
.
value
)
{
if
(
other
.
id
===
candidate
.
id
||
!
other
.
enabled
)
{
continue
;
}
const
otherMethods
=
getProviderVisibleMethods
(
other
);
const
matchedMethod
=
claimedMethods
.
find
((
method
)
=>
otherMethods
.
includes
(
method
),
);
if
(
matchedMethod
)
{
return
{
method
:
matchedMethod
,
conflicting
:
other
,
}
;
}
}
return
null
;
}
function
showProviderEnablementConflict
(
conflict
:
{
method
:
"
alipay
"
|
"
wxpay
"
;
conflicting
:
ProviderInstance
}
,
)
{
appStore
.
showError
(
t
(
"
admin.settings.payment.enableConflict
"
,
{
method
:
t
(
`payment.methods.${conflict.method
}
`
),
provider
:
conflict
.
conflicting
.
name
,
}
),
);
}
async
function
loadProviders
()
{
async
function
loadProviders
()
{
providersLoading
.
value
=
true
;
providersLoading
.
value
=
true
;
try
{
try
{
...
@@ -6619,6 +6515,21 @@ function openEditProvider(provider: ProviderInstance) {
...
@@ -6619,6 +6515,21 @@ function openEditProvider(provider: ProviderInstance) {
async
function
handleSaveProvider
(
payload
:
Partial
<
ProviderInstance
>
)
{
async
function
handleSaveProvider
(
payload
:
Partial
<
ProviderInstance
>
)
{
providerSaving
.
value
=
true
;
providerSaving
.
value
=
true
;
try
{
try
{
const
candidate
:
ProviderEnablementCandidate
=
{
id
:
editingProvider
.
value
?.
id
??
0
,
provider_key
:
payload
.
provider_key
??
editingProvider
.
value
?.
provider_key
??
""
,
supported_types
:
payload
.
supported_types
??
editingProvider
.
value
?.
supported_types
??
[],
enabled
:
payload
.
enabled
??
editingProvider
.
value
?.
enabled
??
false
,
name
:
payload
.
name
??
editingProvider
.
value
?.
name
??
""
,
}
;
const
conflict
=
findProviderEnablementConflict
(
candidate
);
if
(
conflict
)
{
showProviderEnablementConflict
(
conflict
);
return
;
}
if
(
editingProvider
.
value
)
{
if
(
editingProvider
.
value
)
{
await
adminAPI
.
payment
.
updateProvider
(
editingProvider
.
value
.
id
,
payload
);
await
adminAPI
.
payment
.
updateProvider
(
editingProvider
.
value
.
id
,
payload
);
}
else
{
}
else
{
...
@@ -6647,6 +6558,20 @@ async function handleToggleField(
...
@@ -6647,6 +6558,20 @@ async function handleToggleField(
else
if
(
field
===
"
refund_enabled
"
)
newValue
=
!
provider
.
refund_enabled
;
else
if
(
field
===
"
refund_enabled
"
)
newValue
=
!
provider
.
refund_enabled
;
else
newValue
=
!
provider
.
allow_user_refund
;
else
newValue
=
!
provider
.
allow_user_refund
;
if
(
field
===
"
enabled
"
&&
newValue
)
{
const
conflict
=
findProviderEnablementConflict
({
id
:
provider
.
id
,
provider_key
:
provider
.
provider_key
,
supported_types
:
provider
.
supported_types
,
enabled
:
true
,
name
:
provider
.
name
,
}
);
if
(
conflict
)
{
showProviderEnablementConflict
(
conflict
);
return
;
}
}
const
payload
:
Record
<
string
,
boolean
>
=
{
[
field
]:
newValue
}
;
const
payload
:
Record
<
string
,
boolean
>
=
{
[
field
]:
newValue
}
;
// Cascade: turning off refund_enabled also turns off allow_user_refund
// Cascade: turning off refund_enabled also turns off allow_user_refund
if
(
field
===
"
refund_enabled
"
&&
!
newValue
)
{
if
(
field
===
"
refund_enabled
"
&&
!
newValue
)
{
...
@@ -6654,13 +6579,7 @@ async function handleToggleField(
...
@@ -6654,13 +6579,7 @@ async function handleToggleField(
}
}
try
{
try
{
await
adminAPI
.
payment
.
updateProvider
(
provider
.
id
,
payload
);
await
adminAPI
.
payment
.
updateProvider
(
provider
.
id
,
payload
);
if
(
field
===
"
enabled
"
)
provider
.
enabled
=
newValue
;
await
loadProviders
();
else
if
(
field
===
"
refund_enabled
"
)
{
provider
.
refund_enabled
=
newValue
;
if
(
!
newValue
)
provider
.
allow_user_refund
=
false
;
}
else
{
provider
.
allow_user_refund
=
newValue
;
}
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
appStore
.
showError
(
extractApiErrorMessage
(
err
,
t
(
"
common.error
"
),
paymentErrorMap
.
value
),
extractApiErrorMessage
(
err
,
t
(
"
common.error
"
),
paymentErrorMap
.
value
),
...
@@ -6672,11 +6591,22 @@ async function handleToggleType(provider: ProviderInstance, type: string) {
...
@@ -6672,11 +6591,22 @@ async function handleToggleType(provider: ProviderInstance, type: string) {
const
updated
=
provider
.
supported_types
.
includes
(
type
)
const
updated
=
provider
.
supported_types
.
includes
(
type
)
?
provider
.
supported_types
.
filter
((
t
)
=>
t
!==
type
)
?
provider
.
supported_types
.
filter
((
t
)
=>
t
!==
type
)
:
[...
provider
.
supported_types
,
type
];
:
[...
provider
.
supported_types
,
type
];
const
conflict
=
findProviderEnablementConflict
({
id
:
provider
.
id
,
provider_key
:
provider
.
provider_key
,
supported_types
:
updated
,
enabled
:
provider
.
enabled
,
name
:
provider
.
name
,
}
);
if
(
conflict
)
{
showProviderEnablementConflict
(
conflict
);
return
;
}
try
{
try
{
await
adminAPI
.
payment
.
updateProvider
(
provider
.
id
,
{
await
adminAPI
.
payment
.
updateProvider
(
provider
.
id
,
{
supported_types
:
updated
,
supported_types
:
updated
,
}
as
any
);
}
as
any
);
provider
.
supported_types
=
updated
;
await
loadProviders
()
;
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
appStore
.
showError
(
extractApiErrorMessage
(
err
,
t
(
"
common.error
"
),
paymentErrorMap
.
value
),
extractApiErrorMessage
(
err
,
t
(
"
common.error
"
),
paymentErrorMap
.
value
),
...
@@ -6700,11 +6630,7 @@ async function handleReorderProviders(
...
@@ -6700,11 +6630,7 @@ async function handleReorderProviders(
}
as
Partial
<
ProviderInstance
>
),
}
as
Partial
<
ProviderInstance
>
),
),
),
);
);
// Update local state to match new order
await
loadProviders
();
for
(
const
u
of
updates
)
{
const
p
=
providers
.
value
.
find
((
p
)
=>
p
.
id
===
u
.
id
);
if
(
p
)
p
.
sort_order
=
u
.
sort_order
;
}
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
extractApiErrorMessage
(
err
,
t
(
"
common.error
"
)));
appStore
.
showError
(
extractApiErrorMessage
(
err
,
t
(
"
common.error
"
)));
loadProviders
();
loadProviders
();
...
...
frontend/src/views/admin/__tests__/SettingsView.spec.ts
View file @
b22d00e5
...
@@ -17,6 +17,9 @@ const {
...
@@ -17,6 +17,9 @@ const {
getGroups
,
getGroups
,
listProxies
,
listProxies
,
getProviders
,
getProviders
,
updateProvider
,
createProvider
,
deleteProvider
,
fetchPublicSettings
,
fetchPublicSettings
,
adminSettingsFetch
,
adminSettingsFetch
,
showError
,
showError
,
...
@@ -34,6 +37,9 @@ const {
...
@@ -34,6 +37,9 @@ const {
getGroups
:
vi
.
fn
(),
getGroups
:
vi
.
fn
(),
listProxies
:
vi
.
fn
(),
listProxies
:
vi
.
fn
(),
getProviders
:
vi
.
fn
(),
getProviders
:
vi
.
fn
(),
updateProvider
:
vi
.
fn
(),
createProvider
:
vi
.
fn
(),
deleteProvider
:
vi
.
fn
(),
fetchPublicSettings
:
vi
.
fn
(),
fetchPublicSettings
:
vi
.
fn
(),
adminSettingsFetch
:
vi
.
fn
(),
adminSettingsFetch
:
vi
.
fn
(),
showError
:
vi
.
fn
(),
showError
:
vi
.
fn
(),
...
@@ -61,6 +67,9 @@ vi.mock("@/api", () => ({
...
@@ -61,6 +67,9 @@ vi.mock("@/api", () => ({
},
},
payment
:
{
payment
:
{
getProviders
,
getProviders
,
updateProvider
,
createProvider
,
deleteProvider
,
},
},
},
},
}));
}));
...
@@ -413,6 +422,9 @@ describe("admin SettingsView payment visible method controls", () => {
...
@@ -413,6 +422,9 @@ describe("admin SettingsView payment visible method controls", () => {
getGroups
.
mockReset
();
getGroups
.
mockReset
();
listProxies
.
mockReset
();
listProxies
.
mockReset
();
getProviders
.
mockReset
();
getProviders
.
mockReset
();
updateProvider
.
mockReset
();
createProvider
.
mockReset
();
deleteProvider
.
mockReset
();
fetchPublicSettings
.
mockReset
();
fetchPublicSettings
.
mockReset
();
adminSettingsFetch
.
mockReset
();
adminSettingsFetch
.
mockReset
();
showError
.
mockReset
();
showError
.
mockReset
();
...
@@ -467,98 +479,93 @@ describe("admin SettingsView payment visible method controls", () => {
...
@@ -467,98 +479,93 @@ describe("admin SettingsView payment visible method controls", () => {
adminSettingsFetch
.
mockResolvedValue
(
undefined
);
adminSettingsFetch
.
mockResolvedValue
(
undefined
);
});
});
it
(
"
loads canonical source options and normalizes existing value
s
"
,
async
()
=>
{
it
(
"
does not render legacy visible payment method control
s
"
,
async
()
=>
{
const
wrapper
=
mountView
();
const
wrapper
=
mountView
();
await
flushPromises
();
await
flushPromises
();
await
openPaymentTab
(
wrapper
);
await
openPaymentTab
(
wrapper
);
const
paymentSourceSelects
=
wrapper
expect
(
wrapper
.
text
()).
not
.
toContain
(
"
可见方式
"
);
.
findAll
(
"
select.select-stub
"
)
expect
(
wrapper
.
text
()).
not
.
toContain
(
"
支付来源
"
);
.
filter
((
node
)
=>
[
"
alipay
"
,
"
wxpay
"
].
includes
(
node
.
attributes
(
"
data-placeholder
"
)),
);
expect
(
paymentSourceSelects
).
toHaveLength
(
2
);
const
alipaySelect
=
paymentSourceSelects
.
find
(
(
node
)
=>
node
.
attributes
(
"
data-placeholder
"
)
===
"
alipay
"
,
);
const
wxpaySelect
=
paymentSourceSelects
.
find
(
(
node
)
=>
node
.
attributes
(
"
data-placeholder
"
)
===
"
wxpay
"
,
);
expect
(
alipaySelect
?.
element
.
value
).
toBe
(
"
official_alipay
"
);
expect
(
alipaySelect
?.
findAll
(
"
option
"
).
map
((
option
)
=>
option
.
element
.
value
),
).
toEqual
([
""
,
"
official_alipay
"
,
"
easypay_alipay
"
]);
expect
(
wxpaySelect
?.
element
.
value
).
toBe
(
""
);
expect
(
wxpaySelect
?.
findAll
(
"
option
"
).
map
((
option
)
=>
option
.
element
.
value
),
).
toEqual
([
""
,
"
official_wxpay
"
,
"
easypay_wxpay
"
]);
});
});
it
(
"
saves canonical source keys selected from the dropdown
s
"
,
async
()
=>
{
it
(
"
does not submit legacy visible payment method setting
s
"
,
async
()
=>
{
const
wrapper
=
mountView
();
const
wrapper
=
mountView
();
await
flushPromises
();
await
flushPromises
();
await
openPaymentTab
(
wrapper
);
await
openPaymentTab
(
wrapper
);
const
paymentSourceSelects
=
wrapper
.
findAll
(
"
select.select-stub
"
)
.
filter
((
node
)
=>
[
"
alipay
"
,
"
wxpay
"
].
includes
(
node
.
attributes
(
"
data-placeholder
"
)),
);
const
alipaySelect
=
paymentSourceSelects
.
find
(
(
node
)
=>
node
.
attributes
(
"
data-placeholder
"
)
===
"
alipay
"
,
);
const
wxpaySelect
=
paymentSourceSelects
.
find
(
(
node
)
=>
node
.
attributes
(
"
data-placeholder
"
)
===
"
wxpay
"
,
);
await
alipaySelect
?.
setValue
(
"
easypay_alipay
"
);
await
wxpaySelect
?.
setValue
(
"
official_wxpay
"
);
await
wrapper
.
find
(
"
form
"
).
trigger
(
"
submit.prevent
"
);
await
wrapper
.
find
(
"
form
"
).
trigger
(
"
submit.prevent
"
);
await
flushPromises
();
await
flushPromises
();
expect
(
updateSettings
).
toHaveBeenCalledTimes
(
1
);
expect
(
updateSettings
).
toHaveBeenCalledTimes
(
1
);
expect
(
updateSettings
).
toHaveBeenCalledWith
(
const
payload
=
updateSettings
.
mock
.
calls
[
0
]?.[
0
];
expect
.
objectContaining
({
expect
(
payload
).
not
.
toHaveProperty
(
"
payment_visible_method_alipay_source
"
);
payment_visible_method_alipay_source
:
"
easypay_alipay
"
,
expect
(
payload
).
not
.
toHaveProperty
(
"
payment_visible_method_wxpay_source
"
);
payment_visible_method_wxpay_source
:
"
official_wxpay
"
,
expect
(
payload
).
not
.
toHaveProperty
(
"
payment_visible_method_alipay_enabled
"
);
payment_visible_method_alipay_enabled
:
true
,
expect
(
payload
).
not
.
toHaveProperty
(
"
payment_visible_method_wxpay_enabled
"
);
payment_visible_method_wxpay_enabled
:
true
,
}),
);
});
});
it
(
"
blocks saving when a visible payment method is enabled without a source
"
,
async
()
=>
{
it
(
"
updates provider enablement immediately and reloads providers
"
,
async
()
=>
{
const
wrapper
=
mountView
();
const
provider
=
{
id
:
7
,
provider_key
:
"
alipay
"
,
name
:
"
Official Alipay
"
,
config
:
{},
supported_types
:
[
"
alipay
"
],
enabled
:
false
,
payment_mode
:
""
,
refund_enabled
:
false
,
allow_user_refund
:
false
,
limits
:
""
,
sort_order
:
0
,
};
getProviders
.
mockReset
();
getProviders
.
mockResolvedValueOnce
({
data
:
[
provider
]
})
.
mockResolvedValueOnce
({
data
:
[{
...
provider
,
enabled
:
true
}]
});
updateProvider
.
mockResolvedValue
({
data
:
{
...
provider
,
enabled
:
true
}
});
const
PaymentProviderListStub
=
defineComponent
({
emits
:
[
"
toggleField
"
],
setup
(
_
,
{
emit
})
{
return
()
=>
h
(
"
button
"
,
{
class
:
"
provider-toggle-stub
"
,
onClick
:
()
=>
emit
(
"
toggleField
"
,
provider
,
"
enabled
"
),
},
"
toggle provider
"
,
);
},
});
const
wrapper
=
mount
(
SettingsView
,
{
global
:
{
stubs
:
{
AppLayout
:
AppLayoutStub
,
Select
:
SelectStub
,
Toggle
:
ToggleStub
,
Icon
:
true
,
ConfirmDialog
:
true
,
PaymentProviderList
:
PaymentProviderListStub
,
PaymentProviderDialog
:
true
,
GroupBadge
:
true
,
GroupOptionItem
:
true
,
ProxySelector
:
true
,
ImageUpload
:
true
,
BackupSettings
:
true
,
},
},
});
await
flushPromises
();
await
flushPromises
();
await
openPaymentTab
(
wrapper
);
await
openPaymentTab
(
wrapper
);
await
wrapper
.
get
(
"
.provider-toggle-stub
"
).
trigger
(
"
click
"
);
const
paymentSourceSelects
=
wrapper
.
findAll
(
"
select.select-stub
"
)
.
filter
((
node
)
=>
[
"
alipay
"
,
"
wxpay
"
].
includes
(
node
.
attributes
(
"
data-placeholder
"
)),
);
const
alipaySelect
=
paymentSourceSelects
.
find
(
(
node
)
=>
node
.
attributes
(
"
data-placeholder
"
)
===
"
alipay
"
,
);
await
alipaySelect
?.
setValue
(
""
);
await
wrapper
.
find
(
"
form
"
).
trigger
(
"
submit.prevent
"
);
await
flushPromises
();
await
flushPromises
();
expect
(
updateSettings
).
not
.
toHaveBeenCalled
();
expect
(
updateProvider
).
toHaveBeenCalledWith
(
7
,
{
enabled
:
true
});
expect
(
showError
).
toHaveBeenCalled
();
expect
(
getProviders
).
toHaveBeenCalledTimes
(
2
);
expect
(
String
(
showError
.
mock
.
calls
.
at
(
-
1
)?.[
0
]
??
""
)).
toContain
(
"
支付来源
"
,
);
});
});
it
(
"
renders advanced scheduler copy as local experimental gateway policy
"
,
async
()
=>
{
it
(
"
renders advanced scheduler copy as local experimental gateway policy
"
,
async
()
=>
{
...
@@ -588,6 +595,9 @@ describe("admin SettingsView wechat connect controls", () => {
...
@@ -588,6 +595,9 @@ describe("admin SettingsView wechat connect controls", () => {
getGroups
.
mockReset
();
getGroups
.
mockReset
();
listProxies
.
mockReset
();
listProxies
.
mockReset
();
getProviders
.
mockReset
();
getProviders
.
mockReset
();
updateProvider
.
mockReset
();
createProvider
.
mockReset
();
deleteProvider
.
mockReset
();
fetchPublicSettings
.
mockReset
();
fetchPublicSettings
.
mockReset
();
adminSettingsFetch
.
mockReset
();
adminSettingsFetch
.
mockReset
();
showError
.
mockReset
();
showError
.
mockReset
();
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment