package payment import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/integration/walletclient" "hyapp-admin-server/internal/middleware" walletv1 "hyapp.local/api/proto/wallet/v1" "github.com/gin-gonic/gin" ) func TestListThirdPartyPaymentChannelsForwardsIncludeDisabledMethods(t *testing.T) { wallet := &mockPaymentWallet{thirdPartyChannelsResp: &walletv1.ListThirdPartyPaymentChannelsResponse{ Channels: []*walletv1.ThirdPartyPaymentChannel{{ AppCode: "lalu", ProviderCode: "mifapay", ProviderName: "MiFaPay", Status: "active", SortOrder: 10, Methods: []*walletv1.ThirdPartyPaymentMethod{{ MethodId: 810, AppCode: "lalu", ProviderCode: "mifapay", ProviderName: "MiFaPay", CountryCode: "SA", CountryName: "Saudi Arabia", CurrencyCode: "SAR", PayWay: "Card", PayType: "MADA", MethodName: "MADA", Status: "active", UsdToCurrencyRate: "1.00000000", }}, }}, }} router := newPaymentHandlerTestRouter(New(wallet, nil, nil)) request := httptest.NewRequest(http.MethodGet, "/admin/payment/third-party-channels?provider_code=mifapay&status=active", nil) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } if wallet.lastThirdPartyChannels == nil || wallet.lastThirdPartyChannels.GetAppCode() != "lalu" || wallet.lastThirdPartyChannels.GetProviderCode() != "mifapay" || wallet.lastThirdPartyChannels.GetStatus() != "active" || !wallet.lastThirdPartyChannels.GetIncludeDisabledMethods() { t.Fatalf("third party channels request mismatch: %+v", wallet.lastThirdPartyChannels) } var response adminPaymentTestResponse if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { t.Fatalf("decode response failed: %v", err) } items := response.Data["items"].([]any) channel := items[0].(map[string]any) methods := channel["methods"].([]any) if response.Code != 0 || channel["providerCode"] != "mifapay" || len(methods) != 1 || methods[0].(map[string]any)["payType"] != "MADA" { t.Fatalf("third party channels response mismatch: %+v", response) } } func TestSetThirdPartyPaymentMethodStatusForwardsOperator(t *testing.T) { wallet := &mockPaymentWallet{} router := newPaymentHandlerTestRouter(New(wallet, nil, nil)) request := httptest.NewRequest(http.MethodPatch, "/admin/payment/third-party-methods/810/status", bytes.NewBufferString(`{"enabled":false}`)) request.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } if wallet.lastSetMethodStatus == nil || wallet.lastSetMethodStatus.GetMethodId() != 810 || wallet.lastSetMethodStatus.GetEnabled() || wallet.lastSetMethodStatus.GetOperatorUserId() != 7 { t.Fatalf("method status request mismatch: %+v", wallet.lastSetMethodStatus) } } func TestUpdateThirdPartyPaymentRateTrimsRateAndForwardsOperator(t *testing.T) { wallet := &mockPaymentWallet{} router := newPaymentHandlerTestRouter(New(wallet, nil, nil)) request := httptest.NewRequest(http.MethodPatch, "/admin/payment/third-party-rates/810", bytes.NewBufferString(`{"usdToCurrencyRate":" 3.75000000 "}`)) request.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } if wallet.lastUpdateRate == nil || wallet.lastUpdateRate.GetMethodId() != 810 || wallet.lastUpdateRate.GetUsdToCurrencyRate() != "3.75000000" || wallet.lastUpdateRate.GetOperatorUserId() != 7 { t.Fatalf("payment rate request mismatch: %+v", wallet.lastUpdateRate) } } func TestSyncThirdPartyPaymentRatesUpdatesAllMethodsFromFetchedCurrencyRates(t *testing.T) { wallet := &mockPaymentWallet{thirdPartyChannelsResp: &walletv1.ListThirdPartyPaymentChannelsResponse{ Channels: []*walletv1.ThirdPartyPaymentChannel{{ AppCode: "lalu", ProviderCode: "mifapay", ProviderName: "MiFaPay", Status: "active", Methods: []*walletv1.ThirdPartyPaymentMethod{ {MethodId: 810, CurrencyCode: "SAR", Status: "active"}, {MethodId: 811, CurrencyCode: "BHD", Status: "disabled"}, }, }}, }} handler := New(wallet, nil, nil) handler.exchangeRates = fakeExchangeRateClient{result: exchangeRateResult{ Rates: map[string]string{"SAR": "3.75000000", "BHD": "0.37600000"}, SourceNames: []string{"open.er-api.com"}, }} router := newPaymentHandlerTestRouter(handler) request := httptest.NewRequest(http.MethodPost, "/admin/payment/third-party-rates/sync", nil) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } if wallet.lastThirdPartyChannels == nil || !wallet.lastThirdPartyChannels.GetIncludeDisabledMethods() { t.Fatalf("sync must read active and disabled methods: %+v", wallet.lastThirdPartyChannels) } if len(wallet.updateRateRequests) != 2 { t.Fatalf("sync should update every method with a fetched currency rate: %+v", wallet.updateRateRequests) } got := map[int64]string{} for _, req := range wallet.updateRateRequests { if req.GetOperatorUserId() != 7 { t.Fatalf("sync operator mismatch: %+v", req) } got[req.GetMethodId()] = req.GetUsdToCurrencyRate() } if got[810] != "3.75000000" || got[811] != "0.37600000" { t.Fatalf("synced rates mismatch: %+v", got) } var response adminPaymentTestResponse if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { t.Fatalf("decode response failed: %v", err) } if response.Code != 0 || response.Data["updatedCount"].(float64) != 2 { t.Fatalf("sync response mismatch: %+v", response) } } func TestCreateRechargeProductForwardsWebCoinSellerAudience(t *testing.T) { wallet := &mockPaymentWallet{} router := newPaymentHandlerTestRouter(New(wallet, nil, nil)) body := `{"amountUsdt":"10.000000","coinAmount":880000,"productName":"Seller 10 USD","description":"coin seller tier","audienceType":"coin_seller","platform":"web","regionIds":[7100],"enabled":true}` request := httptest.NewRequest(http.MethodPost, "/admin/payment/recharge-products", bytes.NewBufferString(body)) request.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusCreated { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } if wallet.lastCreateProduct == nil || wallet.lastCreateProduct.GetAmountMicro() != 10_000_000 || wallet.lastCreateProduct.GetCoinAmount() != 880000 || wallet.lastCreateProduct.GetAudienceType() != "coin_seller" || wallet.lastCreateProduct.GetPlatform() != "web" || len(wallet.lastCreateProduct.GetRegionIds()) != 1 || wallet.lastCreateProduct.GetRegionIds()[0] != 7100 || !wallet.lastCreateProduct.GetEnabled() || wallet.lastCreateProduct.GetOperatorUserId() != 7 { t.Fatalf("create recharge product request mismatch: %+v", wallet.lastCreateProduct) } } func newPaymentHandlerTestRouter(handler *Handler) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() router.Use(func(c *gin.Context) { // 后台 handler 只依赖 app_code、request_id 和操作者;测试在这里模拟鉴权中间件已完成后的上下文。 c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "lalu")) c.Set(middleware.ContextRequestID, "payment-handler-test") c.Set(middleware.ContextUserID, uint(7)) c.Set(middleware.ContextUsername, "tester") c.Next() }) router.GET("/admin/payment/third-party-channels", handler.ListThirdPartyPaymentChannels) router.PATCH("/admin/payment/third-party-methods/:method_id/status", handler.SetThirdPartyPaymentMethodStatus) router.PATCH("/admin/payment/third-party-rates/:method_id", handler.UpdateThirdPartyPaymentRate) router.POST("/admin/payment/third-party-rates/sync", handler.SyncThirdPartyPaymentRates) router.POST("/admin/payment/recharge-products", handler.CreateRechargeProduct) return router } type adminPaymentTestResponse struct { Code int `json:"code"` Data map[string]any `json:"data"` } type mockPaymentWallet struct { walletclient.Client lastThirdPartyChannels *walletv1.ListThirdPartyPaymentChannelsRequest thirdPartyChannelsResp *walletv1.ListThirdPartyPaymentChannelsResponse lastSetMethodStatus *walletv1.SetThirdPartyPaymentMethodStatusRequest lastUpdateRate *walletv1.UpdateThirdPartyPaymentRateRequest updateRateRequests []*walletv1.UpdateThirdPartyPaymentRateRequest lastCreateProduct *walletv1.CreateRechargeProductRequest } func (m *mockPaymentWallet) ListThirdPartyPaymentChannels(_ context.Context, req *walletv1.ListThirdPartyPaymentChannelsRequest) (*walletv1.ListThirdPartyPaymentChannelsResponse, error) { m.lastThirdPartyChannels = req if m.thirdPartyChannelsResp != nil { return m.thirdPartyChannelsResp, nil } return &walletv1.ListThirdPartyPaymentChannelsResponse{}, nil } func (m *mockPaymentWallet) SetThirdPartyPaymentMethodStatus(_ context.Context, req *walletv1.SetThirdPartyPaymentMethodStatusRequest) (*walletv1.ThirdPartyPaymentMethodResponse, error) { m.lastSetMethodStatus = req status := "disabled" if req.GetEnabled() { status = "active" } return &walletv1.ThirdPartyPaymentMethodResponse{Method: &walletv1.ThirdPartyPaymentMethod{ MethodId: req.GetMethodId(), AppCode: req.GetAppCode(), ProviderCode: "mifapay", ProviderName: "MiFaPay", CountryCode: "SA", PayWay: "Card", PayType: "MADA", Status: status, }}, nil } func (m *mockPaymentWallet) UpdateThirdPartyPaymentRate(_ context.Context, req *walletv1.UpdateThirdPartyPaymentRateRequest) (*walletv1.ThirdPartyPaymentMethodResponse, error) { m.lastUpdateRate = req m.updateRateRequests = append(m.updateRateRequests, req) return &walletv1.ThirdPartyPaymentMethodResponse{Method: &walletv1.ThirdPartyPaymentMethod{ MethodId: req.GetMethodId(), AppCode: req.GetAppCode(), ProviderCode: "mifapay", ProviderName: "MiFaPay", CountryCode: "SA", PayWay: "Card", PayType: "MADA", Status: "active", UsdToCurrencyRate: req.GetUsdToCurrencyRate(), }}, nil } type fakeExchangeRateClient struct { result exchangeRateResult err error } func (f fakeExchangeRateClient) LatestUSDRates(context.Context, []string) (exchangeRateResult, error) { return f.result, f.err } func (m *mockPaymentWallet) CreateRechargeProduct(_ context.Context, req *walletv1.CreateRechargeProductRequest) (*walletv1.RechargeProductResponse, error) { m.lastCreateProduct = req return &walletv1.RechargeProductResponse{Product: &walletv1.RechargeProduct{ AppCode: req.GetAppCode(), ProductId: 9001, ProductCode: "h5_seller_10", ProductName: req.GetProductName(), Description: req.GetDescription(), AudienceType: req.GetAudienceType(), Platform: req.GetPlatform(), AmountMicro: req.GetAmountMicro(), CoinAmount: req.GetCoinAmount(), RegionIds: append([]int64(nil), req.GetRegionIds()...), Enabled: req.GetEnabled(), Status: "active", }}, nil }