fix(payment): respect configured visible method source

This commit is contained in:
IanShaw027
2026-04-22 11:28:58 +08:00
parent 454873221c
commit 9d5e9bbc18
3 changed files with 322 additions and 1 deletions

View File

@@ -31,3 +31,68 @@ func TestUsesOfficialWxpayVisibleMethodDerivesFromEnabledProviderInstance(t *tes
t.Fatal("expected official wxpay visible method to be detected from enabled provider instance")
}
}
func TestUsesOfficialWxpayVisibleMethodRespectsConfiguredSourceWhenMultipleProvidersEnabled(t *testing.T) {
tests := []struct {
name string
source string
wantOfficial bool
}{
{
name: "official source selected",
source: VisibleMethodSourceOfficialWechat,
wantOfficial: true,
},
{
name: "easypay source selected",
source: VisibleMethodSourceEasyPayWechat,
wantOfficial: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
client := newPaymentConfigServiceTestClient(t)
_, err := client.PaymentProviderInstance.Create().
SetProviderKey(payment.TypeWxpay).
SetName("Official WeChat").
SetConfig("{}").
SetSupportedTypes("wxpay").
SetEnabled(true).
SetSortOrder(1).
Save(ctx)
if err != nil {
t.Fatalf("create official wxpay instance: %v", err)
}
_, err = client.PaymentProviderInstance.Create().
SetProviderKey(payment.TypeEasyPay).
SetName("EasyPay WeChat").
SetConfig("{}").
SetSupportedTypes("wxpay").
SetEnabled(true).
SetSortOrder(2).
Save(ctx)
if err != nil {
t.Fatalf("create easypay wxpay instance: %v", err)
}
svc := &PaymentService{
configService: &PaymentConfigService{
entClient: client,
settingRepo: &paymentConfigSettingRepoStub{
values: map[string]string{
SettingPaymentVisibleMethodWxpaySource: tt.source,
},
},
},
}
if got := svc.usesOfficialWxpayVisibleMethod(ctx); got != tt.wantOfficial {
t.Fatalf("usesOfficialWxpayVisibleMethod() = %v, want %v", got, tt.wantOfficial)
}
})
}
}

View File

@@ -14,6 +14,7 @@ import (
"time"
"github.com/Wei-Shaw/sub2api/internal/payment"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
)
func TestNormalizeVisibleMethods(t *testing.T) {
@@ -419,6 +420,211 @@ func TestVisibleMethodLoadBalancerUsesEnabledProviderInstance(t *testing.T) {
}
}
func TestVisibleMethodLoadBalancerUsesConfiguredSourceWhenMultipleProvidersEnabled(t *testing.T) {
t.Parallel()
tests := []struct {
name string
method payment.PaymentType
officialName string
officialTypes string
easyPayName string
easyPayTypes string
sourceSetting string
wantProvider string
}{
{
name: "alipay uses official source",
method: payment.TypeAlipay,
officialName: "Official Alipay",
officialTypes: "alipay",
easyPayName: "EasyPay Alipay",
easyPayTypes: "alipay",
sourceSetting: VisibleMethodSourceOfficialAlipay,
wantProvider: payment.TypeAlipay,
},
{
name: "alipay uses easypay source",
method: payment.TypeAlipay,
officialName: "Official Alipay",
officialTypes: "alipay",
easyPayName: "EasyPay Alipay",
easyPayTypes: "alipay",
sourceSetting: VisibleMethodSourceEasyPayAlipay,
wantProvider: payment.TypeEasyPay,
},
{
name: "wxpay uses official source",
method: payment.TypeWxpay,
officialName: "Official WeChat",
officialTypes: "wxpay",
easyPayName: "EasyPay WeChat",
easyPayTypes: "wxpay",
sourceSetting: VisibleMethodSourceOfficialWechat,
wantProvider: payment.TypeWxpay,
},
{
name: "wxpay uses easypay source",
method: payment.TypeWxpay,
officialName: "Official WeChat",
officialTypes: "wxpay",
easyPayName: "EasyPay WeChat",
easyPayTypes: "wxpay",
sourceSetting: VisibleMethodSourceEasyPayWechat,
wantProvider: payment.TypeEasyPay,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := newPaymentConfigServiceTestClient(t)
officialProviderKey := payment.TypeAlipay
if tt.method == payment.TypeWxpay {
officialProviderKey = payment.TypeWxpay
}
_, err = client.PaymentProviderInstance.Create().
SetProviderKey(officialProviderKey).
SetName(tt.officialName).
SetConfig("{}").
SetSupportedTypes(tt.officialTypes).
SetEnabled(true).
SetSortOrder(1).
Save(ctx)
if err != nil {
t.Fatalf("create official provider: %v", err)
}
_, err = client.PaymentProviderInstance.Create().
SetProviderKey(payment.TypeEasyPay).
SetName(tt.easyPayName).
SetConfig("{}").
SetSupportedTypes(tt.easyPayTypes).
SetEnabled(true).
SetSortOrder(2).
Save(ctx)
if err != nil {
t.Fatalf("create easypay provider: %v", err)
}
inner := &captureLoadBalancer{}
configService := &PaymentConfigService{
entClient: client,
settingRepo: &paymentConfigSettingRepoStub{
values: map[string]string{
visibleMethodSourceSettingKey(tt.method): tt.sourceSetting,
},
},
}
lb := newVisibleMethodLoadBalancer(inner, configService)
_, err = lb.SelectInstance(ctx, "", tt.method, payment.StrategyRoundRobin, 12.5)
if err != nil {
t.Fatalf("SelectInstance returned error: %v", err)
}
if inner.lastProviderKey != tt.wantProvider {
t.Fatalf("lastProviderKey = %q, want %q", inner.lastProviderKey, tt.wantProvider)
}
})
}
}
func TestVisibleMethodLoadBalancerRejectsMissingOrInvalidSourceWhenMultipleProvidersEnabled(t *testing.T) {
t.Parallel()
tests := []struct {
name string
method payment.PaymentType
sourceValue string
wantMessage string
}{
{
name: "missing alipay source",
method: payment.TypeAlipay,
sourceValue: "",
wantMessage: "alipay source is required when the visible method is enabled",
},
{
name: "invalid wxpay source",
method: payment.TypeWxpay,
sourceValue: "stripe",
wantMessage: "wxpay source must be one of the supported payment providers",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := newPaymentConfigServiceTestClient(t)
officialProviderKey := payment.TypeAlipay
officialSupportedTypes := "alipay"
officialName := "Official Alipay"
easyPaySupportedTypes := "alipay"
easyPayName := "EasyPay Alipay"
if tt.method == payment.TypeWxpay {
officialProviderKey = payment.TypeWxpay
officialSupportedTypes = "wxpay"
officialName = "Official WeChat"
easyPaySupportedTypes = "wxpay"
easyPayName = "EasyPay WeChat"
}
_, err := client.PaymentProviderInstance.Create().
SetProviderKey(officialProviderKey).
SetName(officialName).
SetConfig("{}").
SetSupportedTypes(officialSupportedTypes).
SetEnabled(true).
SetSortOrder(1).
Save(ctx)
if err != nil {
t.Fatalf("create official provider: %v", err)
}
_, err = client.PaymentProviderInstance.Create().
SetProviderKey(payment.TypeEasyPay).
SetName(easyPayName).
SetConfig("{}").
SetSupportedTypes(easyPaySupportedTypes).
SetEnabled(true).
SetSortOrder(2).
Save(ctx)
if err != nil {
t.Fatalf("create easypay provider: %v", err)
}
inner := &captureLoadBalancer{}
configService := &PaymentConfigService{
entClient: client,
settingRepo: &paymentConfigSettingRepoStub{
values: map[string]string{
visibleMethodSourceSettingKey(tt.method): tt.sourceValue,
},
},
}
lb := newVisibleMethodLoadBalancer(inner, configService)
_, err = lb.SelectInstance(ctx, "", tt.method, payment.StrategyRoundRobin, 9.9)
if err == nil {
t.Fatal("SelectInstance should reject invalid visible method source configuration")
}
if infraerrors.Reason(err) != "INVALID_PAYMENT_VISIBLE_METHOD_SOURCE" {
t.Fatalf("Reason(err) = %q, want %q", infraerrors.Reason(err), "INVALID_PAYMENT_VISIBLE_METHOD_SOURCE")
}
if infraerrors.Message(err) != tt.wantMessage {
t.Fatalf("Message(err) = %q, want %q", infraerrors.Message(err), tt.wantMessage)
}
})
}
}
func TestVisibleMethodLoadBalancerRejectsMissingEnabledVisibleMethodProvider(t *testing.T) {
t.Parallel()

View File

@@ -82,6 +82,19 @@ func filterEnabledVisibleMethodInstances(instances []*dbent.PaymentProviderInsta
return filtered
}
func selectVisibleMethodInstanceByProviderKey(instances []*dbent.PaymentProviderInstance, providerKey string) *dbent.PaymentProviderInstance {
providerKey = strings.TrimSpace(providerKey)
if providerKey == "" {
return nil
}
for _, inst := range instances {
if strings.EqualFold(strings.TrimSpace(inst.ProviderKey), providerKey) {
return inst
}
}
return nil
}
func buildPaymentProviderConflictError(method string, conflicting *dbent.PaymentProviderInstance) error {
metadata := map[string]string{
"payment_method": NormalizeVisibleMethod(method),
@@ -133,6 +146,32 @@ func (s *PaymentConfigService) validateVisibleMethodEnablementConflicts(
return nil
}
func (s *PaymentConfigService) resolveVisibleMethodSourceProviderKey(ctx context.Context, method string) (string, error) {
method = NormalizeVisibleMethod(method)
sourceKey := visibleMethodSourceSettingKey(method)
rawSource := ""
if s != nil && s.settingRepo != nil && sourceKey != "" {
value, err := s.settingRepo.GetValue(ctx, sourceKey)
if err != nil {
return "", fmt.Errorf("get %s: %w", sourceKey, err)
}
rawSource = value
}
normalizedSource, err := normalizeVisibleMethodSettingSource(method, rawSource, true)
if err != nil {
return "", err
}
providerKey, ok := VisibleMethodProviderKeyForSource(method, normalizedSource)
if !ok {
return "", infraerrors.BadRequest(
"INVALID_PAYMENT_VISIBLE_METHOD_SOURCE",
fmt.Sprintf("%s source must be one of the supported payment providers", method),
)
}
return providerKey, nil
}
func (s *PaymentConfigService) resolveEnabledVisibleMethodInstance(
ctx context.Context,
method string,
@@ -161,6 +200,17 @@ func (s *PaymentConfigService) resolveEnabledVisibleMethodInstance(
case 1:
return matching[0], nil
default:
return nil, buildPaymentProviderConflictError(method, matching[0])
providerKey, err := s.resolveVisibleMethodSourceProviderKey(ctx, method)
if err != nil {
return nil, err
}
selected := selectVisibleMethodInstanceByProviderKey(matching, providerKey)
if selected == nil {
return nil, infraerrors.BadRequest(
"INVALID_PAYMENT_VISIBLE_METHOD_SOURCE",
fmt.Sprintf("%s source has no enabled provider instance", method),
)
}
return selected, nil
}
}