2026-06-22 16:55:22 +08:00

322 lines
12 KiB
Go

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",
}},
}, {
AppCode: "lalu",
ProviderCode: "v5pay",
ProviderName: "V5Pay",
Status: "active",
SortOrder: 20,
Methods: []*walletv1.ThirdPartyPaymentMethod{{
MethodId: 2310,
AppCode: "lalu",
ProviderCode: "v5pay",
ProviderName: "V5Pay",
CountryCode: "SA",
CountryName: "Saudi Arabia",
CurrencyCode: "SAR",
PayWay: "Ewallet",
PayType: "STCPAY",
MethodName: "STC Pay",
Status: "active",
UsdToCurrencyRate: "1.00000000",
}},
}},
}}
router := newPaymentHandlerTestRouter(New(wallet, nil, nil))
request := httptest.NewRequest(http.MethodGet, "/admin/payment/third-party-channels?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() != "" ||
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)
v5Channel := items[1].(map[string]any)
methods := channel["methods"].([]any)
v5Methods := v5Channel["methods"].([]any)
if response.Code != 0 || channel["providerCode"] != "mifapay" || v5Channel["providerCode"] != "v5pay" || len(methods) != 1 || len(v5Methods) != 1 || methods[0].(map[string]any)["payType"] != "MADA" || v5Methods[0].(map[string]any)["payType"] != "STCPAY" {
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"},
},
}, {
AppCode: "lalu",
ProviderCode: "v5pay",
ProviderName: "V5Pay",
Status: "active",
Methods: []*walletv1.ThirdPartyPaymentMethod{
{MethodId: 2310, CurrencyCode: "SAR", Status: "active"},
},
}},
}}
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) != 3 {
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" || got[2310] != "3.75000000" {
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) != 3 {
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
}