2026-07-02 10:53:44 +08:00

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},
}
}