diff --git a/backend/internal/service/payment_order_jsapi_test.go b/backend/internal/service/payment_order_jsapi_test.go index a89d0380..8c5e4fc0 100644 --- a/backend/internal/service/payment_order_jsapi_test.go +++ b/backend/internal/service/payment_order_jsapi_test.go @@ -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) + } + }) + } +} diff --git a/backend/internal/service/payment_resume_service_test.go b/backend/internal/service/payment_resume_service_test.go index ffa55e69..e19e0b99 100644 --- a/backend/internal/service/payment_resume_service_test.go +++ b/backend/internal/service/payment_resume_service_test.go @@ -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() diff --git a/backend/internal/service/payment_visible_method_instances.go b/backend/internal/service/payment_visible_method_instances.go index 477e8e8b..39358522 100644 --- a/backend/internal/service/payment_visible_method_instances.go +++ b/backend/internal/service/payment_visible_method_instances.go @@ -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 } }