180 lines
6.5 KiB
Go
180 lines
6.5 KiB
Go
package wallet_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"hyapp/pkg/xerr"
|
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
|
walletservice "hyapp/services/wallet-service/internal/service/wallet"
|
|
"hyapp/services/wallet-service/internal/testutil/mysqltest"
|
|
)
|
|
|
|
// TestDebitGiftUsesServerPriceAndCreditsGiftPoint 验证送礼只信服务端价格并产生双边分录。
|
|
func TestDebitGiftUsesServerPriceAndCreditsGiftPoint(t *testing.T) {
|
|
repository := mysqltest.NewRepository(t)
|
|
repository.SetBalance(10001, 100)
|
|
repository.SetGiftPrice("rose", "v9", 7, 3, 11)
|
|
svc := walletservice.New(repository)
|
|
|
|
receipt, err := svc.DebitGift(context.Background(), ledger.DebitGiftCommand{
|
|
CommandID: "cmd-gift-v2",
|
|
RoomID: "room-1",
|
|
SenderUserID: 10001,
|
|
TargetUserID: 10002,
|
|
GiftID: "rose",
|
|
GiftCount: 2,
|
|
PriceVersion: "v9",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DebitGift failed: %v", err)
|
|
}
|
|
|
|
if receipt.CoinSpent != 14 || receipt.GiftPointAdded != 6 || receipt.HeatValue != 22 || receipt.BalanceAfter != 86 || receipt.PriceVersion != "v9" {
|
|
t.Fatalf("server price settlement mismatch: %+v", receipt)
|
|
}
|
|
if receipt.TransactionID == "" || receipt.BillingReceiptID == "" {
|
|
t.Fatalf("receipt identifiers missing: %+v", receipt)
|
|
}
|
|
|
|
balances, err := svc.GetBalances(context.Background(), 10001, []string{ledger.AssetCoin})
|
|
if err != nil {
|
|
t.Fatalf("GetBalances sender failed: %v", err)
|
|
}
|
|
if balanceAmount(balances, ledger.AssetCoin) != 86 {
|
|
t.Fatalf("sender COIN balance mismatch: %+v", balances)
|
|
}
|
|
balances, err = svc.GetBalances(context.Background(), 10002, []string{ledger.AssetGiftPoint})
|
|
if err != nil {
|
|
t.Fatalf("GetBalances target failed: %v", err)
|
|
}
|
|
if balanceAmount(balances, ledger.AssetGiftPoint) != 6 {
|
|
t.Fatalf("target GIFT_POINT balance mismatch: %+v", balances)
|
|
}
|
|
if got := repository.CountRows("wallet_entries", "transaction_id = ?", receipt.TransactionID); got != 2 {
|
|
t.Fatalf("gift debit should write two entries, got %d", got)
|
|
}
|
|
if got := repository.CountRows("wallet_outbox", "transaction_id = ?", receipt.TransactionID); got != 3 {
|
|
t.Fatalf("gift debit should write balance and gift outbox events, got %d", got)
|
|
}
|
|
}
|
|
|
|
// TestDebitGiftIsIdempotent 验证扣费命令重复提交时返回原始回执且不重复落账。
|
|
func TestDebitGiftIsIdempotent(t *testing.T) {
|
|
repository := mysqltest.NewRepository(t)
|
|
repository.SetBalance(10001, 100)
|
|
svc := walletservice.New(repository)
|
|
command := ledger.DebitGiftCommand{
|
|
CommandID: "cmd-1",
|
|
RoomID: "room-1",
|
|
SenderUserID: 10001,
|
|
TargetUserID: 10002,
|
|
GiftID: "rose",
|
|
GiftCount: 2,
|
|
}
|
|
|
|
first, err := svc.DebitGift(context.Background(), command)
|
|
if err != nil {
|
|
t.Fatalf("first debit failed: %v", err)
|
|
}
|
|
second, err := svc.DebitGift(context.Background(), command)
|
|
if err != nil {
|
|
t.Fatalf("second debit failed: %v", err)
|
|
}
|
|
|
|
if first.BillingReceiptID != second.BillingReceiptID || second.BalanceAfter != 80 || second.CoinSpent != 20 {
|
|
t.Fatalf("idempotency mismatch: first=%+v second=%+v", first, second)
|
|
}
|
|
if got := repository.CountRows("wallet_transactions", "command_id = ?", command.CommandID); got != 1 {
|
|
t.Fatalf("idempotent command should write one transaction, got %d", got)
|
|
}
|
|
}
|
|
|
|
// TestDebitGiftRejectsCommandPayloadConflict 锁定同 command_id 不同业务 payload 的冲突语义。
|
|
func TestDebitGiftRejectsCommandPayloadConflict(t *testing.T) {
|
|
repository := mysqltest.NewRepository(t)
|
|
repository.SetBalance(10001, 100)
|
|
svc := walletservice.New(repository)
|
|
command := ledger.DebitGiftCommand{CommandID: "cmd-conflict", RoomID: "room-1", SenderUserID: 10001, TargetUserID: 10002, GiftID: "rose", GiftCount: 1}
|
|
if _, err := svc.DebitGift(context.Background(), command); err != nil {
|
|
t.Fatalf("first debit failed: %v", err)
|
|
}
|
|
command.GiftCount = 2
|
|
_, err := svc.DebitGift(context.Background(), command)
|
|
if !xerr.IsCode(err, xerr.LedgerConflict) {
|
|
t.Fatalf("expected LEDGER_CONFLICT, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestDebitGiftInsufficientBalance 锁定余额不足时不写成功交易、分录或 outbox。
|
|
func TestDebitGiftInsufficientBalance(t *testing.T) {
|
|
repository := mysqltest.NewRepository(t)
|
|
repository.SetBalance(10001, 1)
|
|
svc := walletservice.New(repository)
|
|
|
|
_, err := svc.DebitGift(context.Background(), ledger.DebitGiftCommand{
|
|
CommandID: "cmd-2",
|
|
RoomID: "room-1",
|
|
SenderUserID: 10001,
|
|
TargetUserID: 10002,
|
|
GiftID: "rose",
|
|
GiftCount: 1,
|
|
})
|
|
if !xerr.IsCode(err, xerr.InsufficientBalance) {
|
|
t.Fatalf("expected INSUFFICIENT_BALANCE, got %v", err)
|
|
}
|
|
if got := repository.CountRows("wallet_transactions", "command_id = ?", "cmd-2"); got != 0 {
|
|
t.Fatalf("insufficient balance must not write transaction, got %d", got)
|
|
}
|
|
if got := repository.CountRows("wallet_entries", "transaction_id LIKE ?", "wtx_%"); got != 0 {
|
|
t.Fatalf("insufficient balance must not write entries, got %d", got)
|
|
}
|
|
if got := repository.CountRows("wallet_outbox", "command_id = ?", "cmd-2"); got != 0 {
|
|
t.Fatalf("insufficient balance must not write outbox, got %d", got)
|
|
}
|
|
}
|
|
|
|
// TestAdminCreditAssetIsIdempotent 验证后台入账有审计字段、幂等和余额查询闭环。
|
|
func TestAdminCreditAssetIsIdempotent(t *testing.T) {
|
|
repository := mysqltest.NewRepository(t)
|
|
svc := walletservice.New(repository)
|
|
command := ledger.AdminCreditAssetCommand{
|
|
CommandID: "cmd-admin-credit",
|
|
TargetUserID: 20001,
|
|
AssetType: "coin",
|
|
Amount: 500,
|
|
OperatorUserID: 90001,
|
|
Reason: "manual recharge",
|
|
EvidenceRef: "ticket-1",
|
|
}
|
|
|
|
first, txID, err := svc.AdminCreditAsset(context.Background(), command)
|
|
if err != nil {
|
|
t.Fatalf("AdminCreditAsset failed: %v", err)
|
|
}
|
|
second, secondTxID, err := svc.AdminCreditAsset(context.Background(), command)
|
|
if err != nil {
|
|
t.Fatalf("AdminCreditAsset retry failed: %v", err)
|
|
}
|
|
if txID == "" || txID != secondTxID || first.AvailableAmount != 500 || second.AvailableAmount != 500 || first.AssetType != ledger.AssetCoin {
|
|
t.Fatalf("admin credit idempotency mismatch: first=%+v second=%+v tx=%s/%s", first, second, txID, secondTxID)
|
|
}
|
|
|
|
balances, err := svc.GetBalances(context.Background(), 20001, []string{ledger.AssetCoin, ledger.AssetGiftPoint})
|
|
if err != nil {
|
|
t.Fatalf("GetBalances failed: %v", err)
|
|
}
|
|
if balanceAmount(balances, ledger.AssetCoin) != 500 || balanceAmount(balances, ledger.AssetGiftPoint) != 0 {
|
|
t.Fatalf("balances mismatch: %+v", balances)
|
|
}
|
|
}
|
|
|
|
func balanceAmount(balances []ledger.AssetBalance, assetType string) int64 {
|
|
for _, balance := range balances {
|
|
if balance.AssetType == assetType {
|
|
return balance.AvailableAmount
|
|
}
|
|
}
|
|
return -1
|
|
}
|