package coinledger import "testing" func TestNormalizeListQueryCapsPageSize(t *testing.T) { query := normalizeListQuery(listQuery{Page: -1, PageSize: 1000, UserKeyword: " 10001 "}) if query.Page != 1 || query.PageSize != 100 || query.UserKeyword != "10001" { t.Fatalf("normalized query mismatch: %+v", query) } } func TestCoinLedgerWhereUsesTimeWindowAndUserFilter(t *testing.T) { query := listQuery{StartAtMS: 100, EndAtMS: 200} where, args := coinLedgerWhere("lalu", query, []int64{11, 22}) if want := "WHERE e.app_code = ? AND e.asset_type = ? AND e.created_at_ms >= ? AND e.created_at_ms < ? AND e.user_id IN (?,?)"; where != want { t.Fatalf("where mismatch:\nwant %s\n got %s", want, where) } if len(args) != 6 || args[0] != "lalu" || args[1] != coinAssetType || args[2] != int64(100) || args[3] != int64(200) || args[4] != int64(11) || args[5] != int64(22) { t.Fatalf("args mismatch: %#v", args) } } func TestCoinAdjustmentWhereLimitsManualCreditCoinEntries(t *testing.T) { query := listQuery{StartAtMS: 100, EndAtMS: 200} where, args := coinAdjustmentWhere("lalu", query, []int64{11}) if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type = ? AND e.created_at_ms >= ? AND e.created_at_ms < ? AND e.user_id IN (?)"; where != want { t.Fatalf("where mismatch:\nwant %s\n got %s", want, where) } if len(args) != 6 || args[0] != "lalu" || args[1] != coinAssetType || args[2] != coinManualCreditBizType || args[3] != int64(100) || args[4] != int64(200) || args[5] != int64(11) { t.Fatalf("args mismatch: %#v", args) } } func TestCoinSellerLedgerWhereUsesExactSellerAndType(t *testing.T) { query := coinSellerLedgerQuery{SellerUserID: 3001, LedgerType: coinSellerLedgerTypeSellerTransfer, StartAtMS: 100, EndAtMS: 200} where, args, err := coinSellerLedgerWhere("lalu", query, []int64{3001}) if err != nil { t.Fatalf("coin seller ledger where failed: %v", err) } if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type = ? AND e.created_at_ms >= ? AND e.created_at_ms < ? AND e.user_id IN (?)"; where != want { t.Fatalf("where mismatch:\nwant %s\n got %s", want, where) } if len(args) != 6 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerTransferBizType || args[3] != int64(100) || args[4] != int64(200) || args[5] != int64(3001) { t.Fatalf("args mismatch: %#v", args) } } func TestCoinSellerLedgerWhereMapsAdminStockCredit(t *testing.T) { query := coinSellerLedgerQuery{LedgerType: coinSellerLedgerTypeAdminStockCredit} where, args, err := coinSellerLedgerWhere("lalu", query, []int64{3001, 3002}) if err != nil { t.Fatalf("coin seller ledger where failed: %v", err) } if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?,?,?) AND e.user_id IN (?,?)"; where != want { t.Fatalf("where mismatch:\nwant %s\n got %s", want, where) } if len(args) != 8 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerRechargeBizType || args[4] != coinSellerCoinCompensationBizType || args[5] != coinManualCreditBizType || args[6] != int64(3001) || args[7] != int64(3002) { t.Fatalf("args mismatch: %#v", args) } } func TestCoinSellerLedgerWhereEmptyTypeUsesAllPublicTypes(t *testing.T) { where, args, err := coinSellerLedgerWhere("lalu", coinSellerLedgerQuery{}, nil) if err != nil { t.Fatalf("coin seller ledger where failed: %v", err) } if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?,?,?,?,?)"; where != want { t.Fatalf("where mismatch:\nwant %s\n got %s", want, where) } if len(args) != 8 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerRechargeBizType || args[4] != coinSellerCoinCompensationBizType || args[5] != coinSellerTransferBizType || args[6] != salaryTransferToCoinSellerBizType || args[7] != coinManualCreditBizType { t.Fatalf("args mismatch: %#v", args) } } func TestCoinSellerLedgerWhereRejectsInvalidType(t *testing.T) { _, _, err := coinSellerLedgerWhere("lalu", coinSellerLedgerQuery{LedgerType: "bad_type"}, nil) if err == nil { t.Fatalf("expected invalid ledger type error") } } func TestCoinSellerLedgerReceiverUserIDUsesActualReceiver(t *testing.T) { metadata := map[string]any{"target_user_id": float64(4001)} if got := coinSellerLedgerReceiverUserID(coinSellerTransferBizType, 3001, 0, metadata); got != 4001 { t.Fatalf("seller transfer receiver mismatch: %d", got) } if got := coinSellerLedgerReceiverUserID(coinSellerStockPurchaseBizType, 3001, 4001, metadata); got != 3001 { t.Fatalf("admin stock receiver mismatch: %d", got) } if got := coinSellerLedgerReceiverUserID(salaryTransferToCoinSellerBizType, 3001, 4001, metadata); got != 3001 { t.Fatalf("salary transfer receiver mismatch: %d", got) } } func TestCoinSellerLedgerOperatorExportFieldsOnlyForAdminStock(t *testing.T) { adminItem := coinSellerLedgerDTO{ LedgerType: coinSellerLedgerTypeAdminStockCredit, OperatorUserID: "7", Operator: coinAdjustmentOperatorDTO{AdminID: "7", Username: "hyappadmin", Name: "Admin"}, } operatorName, operatorID := coinSellerLedgerOperatorExportFields(adminItem) if operatorName != "hyappadmin" || operatorID != "7" { t.Fatalf("operator fields mismatch: name=%q id=%q", operatorName, operatorID) } transferItem := coinSellerLedgerDTO{LedgerType: coinSellerLedgerTypeSellerTransfer, OperatorUserID: "7"} operatorName, operatorID = coinSellerLedgerOperatorExportFields(transferItem) if operatorName != "" || operatorID != "" { t.Fatalf("seller transfer should not export operator: name=%q id=%q", operatorName, operatorID) } } func TestCoinSellerLedgerLabelUsesConcreteStockAndDebitType(t *testing.T) { cases := []struct { name string item coinSellerLedgerDTO want string }{ {name: "usdt purchase", item: coinSellerLedgerDTO{BizType: coinSellerStockPurchaseBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "USDT进货"}, {name: "third party recharge", item: coinSellerLedgerDTO{BizType: coinSellerRechargeBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "三方充值"}, {name: "compensation", item: coinSellerLedgerDTO{BizType: coinSellerCoinCompensationBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "金币补偿"}, {name: "debit", item: coinSellerLedgerDTO{BizType: coinManualCreditBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit, AvailableDelta: -100}, want: "金币扣除"}, {name: "manual increase", item: coinSellerLedgerDTO{BizType: coinManualCreditBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit, AvailableDelta: 100}, want: "金币增加"}, } for _, tc := range cases { if got := coinSellerLedgerLabel(tc.item); got != tc.want { t.Fatalf("%s label mismatch: got %q want %q", tc.name, got, tc.want) } } } func TestCoinSellerLedgerCSVAmountFormatters(t *testing.T) { if got := formatUSDMinorForCSV(50); got != "0.50" { t.Fatalf("usd minor format mismatch: %q", got) } if got := formatUSDTMicroForCSV(1_230_000); got != "1.23" { t.Fatalf("usdt micro format mismatch: %q", got) } } func TestNormalizeCoinSellerLedgerQueryCapsPageSize(t *testing.T) { query := normalizeCoinSellerLedgerQuery(coinSellerLedgerQuery{Page: -1, PageSize: 1000, SellerKeyword: " 164425 ", LedgerType: " seller_transfer "}) if query.Page != 1 || query.PageSize != 100 || query.SellerKeyword != "164425" || query.LedgerType != coinSellerLedgerTypeSellerTransfer { t.Fatalf("normalized query mismatch: %+v", query) } } func TestDirectionAndAmountForDelta(t *testing.T) { if directionForDelta(-9) != directionOut || absInt64(-9) != 9 { t.Fatalf("expense projection mismatch") } if directionForDelta(12) != directionIn || absInt64(12) != 12 { t.Fatalf("income projection mismatch") } } func TestParseMetadataJSON(t *testing.T) { metadata, err := parseMetadataJSON(`{"gift_name":"Rose","gift_count":2,"target_user_id":1002}`) if err != nil { t.Fatalf("parse metadata failed: %v", err) } if metadata["gift_name"] != "Rose" || metadata["gift_count"] != float64(2) || metadata["target_user_id"] != float64(1002) { t.Fatalf("metadata mismatch: %#v", metadata) } empty, err := parseMetadataJSON("null") if err != nil { t.Fatalf("parse empty metadata failed: %v", err) } if len(empty) != 0 { t.Fatalf("empty metadata mismatch: %#v", empty) } }