270 lines
10 KiB
Go
270 lines
10 KiB
Go
package financeapplication
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"hyapp-admin-server/internal/appctx"
|
|
"hyapp-admin-server/internal/integration/userclient"
|
|
"hyapp-admin-server/internal/integration/walletclient"
|
|
"hyapp-admin-server/internal/model"
|
|
"hyapp-admin-server/internal/modules/shared"
|
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func TestNormalizeCreateInputRequiresOperationPermission(t *testing.T) {
|
|
service := NewService(nil)
|
|
_, err := service.normalizeCreateInput(shared.Actor{
|
|
UserID: 7,
|
|
Username: "ops",
|
|
Permissions: []string{permissionCreateApplication},
|
|
}, validCreateRequest())
|
|
if err == nil || err.Error() != "没有操作权限" {
|
|
t.Fatalf("operation permission must be required, got err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeCreateInputRequiresWalletIdentityForWalletOperation(t *testing.T) {
|
|
service := NewService(nil)
|
|
req := validCreateRequest()
|
|
req.Operation = "user_wallet_debit"
|
|
_, err := service.normalizeCreateInput(actorWithPermissions("finance-operation:user-wallet-debit"), req)
|
|
if err == nil || err.Error() != "钱包身份不正确" {
|
|
t.Fatalf("wallet operation must require wallet identity, got err=%v", err)
|
|
}
|
|
|
|
req.WalletIdentity = "agency"
|
|
item, err := service.normalizeCreateInput(actorWithPermissions("finance-operation:user-wallet-debit"), req)
|
|
if err != nil {
|
|
t.Fatalf("wallet identity should be accepted: %v", err)
|
|
}
|
|
if item.WalletIdentity != "agency" {
|
|
t.Fatalf("wallet identity mismatch: %q", item.WalletIdentity)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeCreateInputClearsWalletIdentityForCoinOperation(t *testing.T) {
|
|
service := NewService(nil)
|
|
req := validCreateRequest()
|
|
req.WalletIdentity = "host"
|
|
item, err := service.normalizeCreateInput(actorWithPermissions("finance-operation:coin-seller-coin-credit"), req)
|
|
if err != nil {
|
|
t.Fatalf("coin operation should be accepted: %v", err)
|
|
}
|
|
if item.WalletIdentity != "" {
|
|
t.Fatalf("non-wallet operation must clear wallet identity, got %q", item.WalletIdentity)
|
|
}
|
|
if item.RechargeAmount != "10.50" {
|
|
t.Fatalf("recharge amount mismatch: %q", item.RechargeAmount)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeCreateInputAllowsZeroRechargeAmountForUserCoinOperation(t *testing.T) {
|
|
service := NewService(nil)
|
|
req := validCreateRequest()
|
|
req.Operation = "user_coin_credit"
|
|
req.RechargeAmount = 0
|
|
|
|
item, err := service.normalizeCreateInput(actorWithPermissions("finance-operation:user-coin-credit"), req)
|
|
if err != nil {
|
|
t.Fatalf("user coin operation should allow zero recharge amount: %v", err)
|
|
}
|
|
if item.RechargeAmount != "0.00" {
|
|
t.Fatalf("zero recharge amount must be persisted with decimal scale, got %q", item.RechargeAmount)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeCreateInputRejectsZeroRechargeAmountForWalletAndCoinSellerOperations(t *testing.T) {
|
|
service := NewService(nil)
|
|
tests := []struct {
|
|
name string
|
|
operation string
|
|
permission string
|
|
identity string
|
|
}{
|
|
{name: "wallet", operation: "user_wallet_credit", permission: "finance-operation:user-wallet-credit", identity: "agency"},
|
|
{name: "coin seller", operation: "coin_seller_coin_credit", permission: "finance-operation:coin-seller-coin-credit"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := validCreateRequest()
|
|
req.Operation = tt.operation
|
|
req.WalletIdentity = tt.identity
|
|
req.RechargeAmount = 0
|
|
_, err := service.normalizeCreateInput(actorWithPermissions(tt.permission), req)
|
|
if err == nil || err.Error() != "充值金额不正确" {
|
|
t.Fatalf("operation %s must still require positive recharge amount, got err=%v", tt.operation, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteApprovedApplicationAdjustsUserCoinDebit(t *testing.T) {
|
|
wallet := &fakeFinanceWallet{}
|
|
service := NewService(nil, WithUserClient(&fakeFinanceUser{
|
|
displays: map[string]int64{"163001": 312899006709637120},
|
|
users: map[int64]*userclient.User{
|
|
312899006709637120: {UserID: 312899006709637120, CountryID: 63, RegionID: 7},
|
|
},
|
|
}), WithWalletClient(wallet))
|
|
application := model.FinanceApplication{
|
|
ID: 9,
|
|
AppCode: "lalu",
|
|
Operation: "user_coin_debit",
|
|
TargetUserID: "163001",
|
|
CoinAmount: 120,
|
|
RechargeAmount: "10.00",
|
|
}
|
|
|
|
execution, err := service.executeApprovedApplication(appctx.WithContext(context.Background(), "lalu"), shared.Actor{UserID: 7}, application, "req-1")
|
|
if err != nil {
|
|
t.Fatalf("execute approved application failed: %v", err)
|
|
}
|
|
if wallet.assetReq == nil || wallet.assetReq.GetAssetType() != financeAssetCoin || wallet.assetReq.GetAmount() != -120 || wallet.assetReq.GetTargetUserId() != 312899006709637120 {
|
|
t.Fatalf("wallet asset request mismatch: %+v", wallet.assetReq)
|
|
}
|
|
if execution.CommandID != "finance-application:9:user_coin_debit" || execution.AmountDelta != -120 || execution.BalanceAfter != 880 {
|
|
t.Fatalf("execution mismatch: %+v", execution)
|
|
}
|
|
}
|
|
|
|
func TestExecuteApprovedApplicationUsesWalletIdentityAmountMinor(t *testing.T) {
|
|
wallet := &fakeFinanceWallet{}
|
|
service := NewService(nil, WithUserClient(&fakeFinanceUser{
|
|
users: map[int64]*userclient.User{
|
|
312899006709637120: {UserID: 312899006709637120, CountryID: 63, RegionID: 7},
|
|
},
|
|
summaries: map[int64]*userclient.UserRoleSummary{
|
|
312899006709637120: {UserID: 312899006709637120, IsAgency: true},
|
|
},
|
|
}), WithWalletClient(wallet))
|
|
application := model.FinanceApplication{
|
|
ID: 10,
|
|
AppCode: "lalu",
|
|
Operation: "user_wallet_credit",
|
|
WalletIdentity: "agency",
|
|
TargetUserID: "312899006709637120",
|
|
CoinAmount: 999,
|
|
RechargeAmount: "12.34",
|
|
}
|
|
|
|
_, err := service.executeApprovedApplication(appctx.WithContext(context.Background(), "lalu"), shared.Actor{UserID: 8}, application, "req-2")
|
|
if err != nil {
|
|
t.Fatalf("execute wallet application failed: %v", err)
|
|
}
|
|
if wallet.assetReq == nil || wallet.assetReq.GetAssetType() != financeAssetAgencySalaryUSD || wallet.assetReq.GetAmount() != 1234 {
|
|
t.Fatalf("identity wallet request must use salary minor amount, got %+v", wallet.assetReq)
|
|
}
|
|
}
|
|
|
|
func TestExecuteApprovedApplicationAdjustsCoinSellerStockDeduction(t *testing.T) {
|
|
wallet := &fakeFinanceWallet{}
|
|
service := NewService(nil, WithUserClient(&fakeFinanceUser{
|
|
users: map[int64]*userclient.User{
|
|
312899006709637120: {UserID: 312899006709637120, CountryID: 63, RegionID: 7},
|
|
},
|
|
summaries: map[int64]*userclient.UserRoleSummary{
|
|
312899006709637120: {UserID: 312899006709637120, IsCoinSeller: true, CoinSellerStatus: "active"},
|
|
},
|
|
}), WithWalletClient(wallet))
|
|
application := model.FinanceApplication{
|
|
ID: 11,
|
|
AppCode: "lalu",
|
|
Operation: "coin_seller_coin_debit",
|
|
TargetUserID: "312899006709637120",
|
|
CoinAmount: 3000000,
|
|
RechargeAmount: "37.50",
|
|
}
|
|
|
|
execution, err := service.executeApprovedApplication(appctx.WithContext(context.Background(), "lalu"), shared.Actor{UserID: 9}, application, "req-3")
|
|
if err != nil {
|
|
t.Fatalf("execute coin seller application failed: %v", err)
|
|
}
|
|
if wallet.stockReq == nil || wallet.stockReq.GetStockType() != financeStockTypeUSDTDeduction || wallet.stockReq.GetCoinAmount() != 3000000 || wallet.stockReq.GetPaidAmountMicro() != 37500000 {
|
|
t.Fatalf("coin seller stock request mismatch: %+v", wallet.stockReq)
|
|
}
|
|
if wallet.stockReq.GetSellerCountryId() != 63 || wallet.stockReq.GetSellerRegionId() != 7 {
|
|
t.Fatalf("coin seller region snapshot mismatch: %+v", wallet.stockReq)
|
|
}
|
|
if execution.AssetType != financeAssetCoinSellerCoin || execution.AmountDelta != -3000000 || execution.BalanceAfter != 7000000 {
|
|
t.Fatalf("coin seller execution mismatch: %+v", execution)
|
|
}
|
|
}
|
|
|
|
func validCreateRequest() createApplicationRequest {
|
|
return createApplicationRequest{
|
|
AppCode: "lalu",
|
|
Operation: "coin_seller_coin_credit",
|
|
TargetUserID: "10001",
|
|
CoinAmount: 100,
|
|
RechargeAmount: 10.5,
|
|
CredentialImageURL: "https://example.com/receipt.png",
|
|
}
|
|
}
|
|
|
|
type fakeFinanceUser struct {
|
|
userclient.Client
|
|
users map[int64]*userclient.User
|
|
displays map[string]int64
|
|
summaries map[int64]*userclient.UserRoleSummary
|
|
}
|
|
|
|
func (f *fakeFinanceUser) GetUser(_ context.Context, req userclient.GetUserRequest) (*userclient.User, error) {
|
|
if user := f.users[req.UserID]; user != nil {
|
|
return user, nil
|
|
}
|
|
return nil, status.Error(codes.NotFound, "user not found")
|
|
}
|
|
|
|
func (f *fakeFinanceUser) ResolveDisplayUserID(_ context.Context, req userclient.ResolveDisplayUserIDRequest) (*userclient.UserIdentity, error) {
|
|
if userID := f.displays[req.DisplayUserID]; userID > 0 {
|
|
return &userclient.UserIdentity{UserID: userID, DisplayUserID: req.DisplayUserID}, nil
|
|
}
|
|
return nil, status.Error(codes.NotFound, "display user id not found")
|
|
}
|
|
|
|
func (f *fakeFinanceUser) GetUserRoleSummary(_ context.Context, req userclient.GetUserRoleSummaryRequest) (*userclient.UserRoleSummary, error) {
|
|
if summary := f.summaries[req.UserID]; summary != nil {
|
|
return summary, nil
|
|
}
|
|
return &userclient.UserRoleSummary{UserID: req.UserID}, nil
|
|
}
|
|
|
|
type fakeFinanceWallet struct {
|
|
walletclient.Client
|
|
assetReq *walletv1.AdminCreditAssetRequest
|
|
stockReq *walletv1.AdminCreditCoinSellerStockRequest
|
|
}
|
|
|
|
func (f *fakeFinanceWallet) AdminCreditAsset(_ context.Context, req *walletv1.AdminCreditAssetRequest) (*walletv1.AdminCreditAssetResponse, error) {
|
|
f.assetReq = req
|
|
return &walletv1.AdminCreditAssetResponse{
|
|
TransactionId: "tx-" + req.GetCommandId(),
|
|
Balance: &walletv1.AssetBalance{AvailableAmount: 880},
|
|
}, nil
|
|
}
|
|
|
|
func (f *fakeFinanceWallet) AdminCreditCoinSellerStock(_ context.Context, req *walletv1.AdminCreditCoinSellerStockRequest) (*walletv1.AdminCreditCoinSellerStockResponse, error) {
|
|
f.stockReq = req
|
|
coinAmount := req.GetCoinAmount()
|
|
if req.GetStockType() == financeStockTypeUSDTDeduction {
|
|
coinAmount = -coinAmount
|
|
}
|
|
return &walletv1.AdminCreditCoinSellerStockResponse{
|
|
TransactionId: "tx-" + req.GetCommandId(),
|
|
CoinAmount: coinAmount,
|
|
BalanceAfter: 7000000,
|
|
}, nil
|
|
}
|
|
|
|
func actorWithPermissions(operationPermission string) shared.Actor {
|
|
return shared.Actor{
|
|
UserID: 7,
|
|
Username: "ops",
|
|
Permissions: []string{permissionCreateApplication, operationPermission},
|
|
}
|
|
}
|