222 lines
6.9 KiB
Go

package app
import (
"context"
"encoding/json"
"runtime"
"testing"
"time"
"google.golang.org/protobuf/proto"
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
"hyapp/internal/testutil/mysqlschema"
"hyapp/pkg/gamemq"
"hyapp/pkg/roommq"
"hyapp/pkg/walletmq"
mysqlstorage "hyapp/services/statistics-service/internal/storage/mysql"
)
func TestLocalStatisticsDataScreenFlow(t *testing.T) {
ctx := context.Background()
_, file, _, _ := runtime.Caller(0)
schema := mysqlschema.New(t, mysqlschema.Config{
InitDBPath: mysqlschema.InitDBPath(t, file, "../../deploy/mysql/initdb/001_statistics_service.sql"),
DatabasePrefix: "hy_stats_flow",
})
repo, err := mysqlstorage.Open(ctx, schema.DSN)
if err != nil {
t.Fatalf("open statistics repository failed: %v", err)
}
t.Cleanup(func() { _ = repo.Close() })
occurredAt := time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC).UnixMilli()
dayStart := time.Date(2026, 5, 31, 0, 0, 0, 0, time.UTC).UnixMilli()
const (
appCode = "lalu"
regionID = int64(210)
userID = int64(42)
)
googleRecharge := walletMessage(t, walletmq.WalletOutboxMessage{
AppCode: appCode,
EventID: "WalletRechargeRecorded:google:1",
EventType: "WalletRechargeRecorded",
TransactionID: "wallet_tx_google_1",
CommandID: "google_order_1",
UserID: userID,
PayloadJSON: mustJSON(t, map[string]any{
"amount_micro": int64(1_500_000),
"region_id": regionID,
"channel": "google",
"recharge_sequence": int64(1),
}),
OccurredAtMS: occurredAt,
})
recharge, ok, err := rechargeEvent(googleRecharge)
if err != nil || !ok {
t.Fatalf("decode google recharge failed: ok=%v err=%v", ok, err)
}
if err := repo.ConsumeRecharge(ctx, recharge); err != nil {
t.Fatalf("consume google recharge failed: %v", err)
}
roomGiftBody := roomGiftMessage(t, appCode, "room_gift_1", "room_1", occurredAt, &roomeventsv1.RoomGiftSent{
SenderUserId: userID,
TargetUserId: 99,
GiftId: "rose",
GiftCount: 10,
GiftValue: 1_000,
VisibleRegionId: regionID,
CommandId: "gift_command_1",
PoolId: "lucky_100",
})
gift, ok, err := roomGiftEvent(roomGiftBody)
if err != nil || !ok {
t.Fatalf("decode room gift failed: ok=%v err=%v", ok, err)
}
if err := repo.ConsumeRoomGift(ctx, gift); err != nil {
t.Fatalf("consume room gift failed: %v", err)
}
luckyReward := walletMessage(t, walletmq.WalletOutboxMessage{
AppCode: appCode,
EventID: "WalletLuckyGiftRewardCredited:1",
EventType: "WalletLuckyGiftRewardCredited",
TransactionID: "wallet_tx_lucky_1",
CommandID: "lucky_reward:lucky_draw_1",
UserID: userID,
PayloadJSON: mustJSON(t, map[string]any{
"user_id": userID,
"draw_id": "lucky_draw_1",
"room_id": "room_1",
"pool_id": "lucky_100",
"amount": int64(300),
"visible_region_id": regionID,
}),
OccurredAtMS: occurredAt,
})
reward, ok, err := luckyGiftRewardEvent(luckyReward)
if err != nil || !ok {
t.Fatalf("decode lucky reward failed: ok=%v err=%v", ok, err)
}
if err := repo.ConsumeLuckyGiftReward(ctx, reward); err != nil {
t.Fatalf("consume lucky reward failed: %v", err)
}
for _, message := range []gamemq.GameOutboxMessage{
gameMessage(appCode, "game_order_debit_1", userID, "debit", 700, regionID, occurredAt),
gameMessage(appCode, "game_order_credit_1", userID, "credit", 200, regionID, occurredAt),
} {
body, err := gamemq.EncodeGameOutboxMessage(message)
if err != nil {
t.Fatalf("encode game message failed: %v", err)
}
event, ok, err := gameOrderEvent(body)
if err != nil || !ok {
t.Fatalf("decode game order failed: ok=%v err=%v", ok, err)
}
if err := repo.ConsumeGameOrder(ctx, event); err != nil {
t.Fatalf("consume game order failed: %v", err)
}
}
overview, err := repo.QueryOverview(ctx, mysqlstorage.OverviewQuery{
AppCode: appCode,
StartMS: dayStart,
EndMS: dayStart + int64(24*time.Hour/time.Millisecond),
CountryID: regionID,
})
if err != nil {
t.Fatalf("query overview failed: %v", err)
}
if overview.GoogleRechargeUSDMinor != 150 || overview.RechargeUSDMinor != 150 || overview.NewUserRechargeUSDMinor != 150 {
t.Fatalf("google recharge metrics mismatch: %+v", overview)
}
if overview.LuckyGiftTurnover != 1_000 || overview.LuckyGiftPayout != 300 || overview.LuckyGiftProfit != 700 || overview.LuckyGiftPayers != 1 {
t.Fatalf("lucky gift metrics mismatch: %+v", overview)
}
if len(overview.LuckyGiftPools) != 1 {
t.Fatalf("lucky gift pool breakdown missing: %+v", overview.LuckyGiftPools)
}
pool := overview.LuckyGiftPools[0]
if pool.PoolID != "lucky_100" || pool.TurnoverCoin != 1_000 || pool.PayoutCoin != 300 || pool.ProfitCoin != 700 {
t.Fatalf("lucky gift pool metrics mismatch: %+v", pool)
}
if overview.GameTurnover != 700 || overview.GamePayout != 200 || overview.GameProfit != 500 || overview.GamePlayers != 1 {
t.Fatalf("game metrics mismatch: %+v", overview)
}
}
func walletMessage(t *testing.T, message walletmq.WalletOutboxMessage) []byte {
t.Helper()
body, err := walletmq.EncodeWalletOutboxMessage(message)
if err != nil {
t.Fatalf("encode wallet message failed: %v", err)
}
return body
}
func roomGiftMessage(t *testing.T, appCode string, eventID string, roomID string, occurredAt int64, gift *roomeventsv1.RoomGiftSent) []byte {
t.Helper()
giftBody, err := proto.Marshal(gift)
if err != nil {
t.Fatalf("marshal gift failed: %v", err)
}
body, err := roommq.EncodeRoomOutboxMessage(&roomeventsv1.EventEnvelope{
EventId: eventID,
RoomId: roomID,
EventType: "RoomGiftSent",
RoomVersion: 1,
OccurredAtMs: occurredAt,
Body: giftBody,
AppCode: appCode,
})
if err != nil {
t.Fatalf("encode room message failed: %v", err)
}
return body
}
func gameMessage(appCode string, orderID string, userID int64, opType string, amount int64, regionID int64, occurredAt int64) gamemq.GameOutboxMessage {
return gamemq.GameOutboxMessage{
AppCode: appCode,
EventID: "GameOrderSettled:" + orderID,
EventType: "GameOrderSettled",
OrderID: orderID,
UserID: userID,
PlatformCode: "yomi",
GameID: "fish_001",
OpType: opType,
CoinAmount: amount,
PayloadJSON: mustJSONNoT(map[string]any{
"order_id": orderID,
"platform_code": "yomi",
"game_id": "fish_001",
"user_id": userID,
"room_id": "room_1",
"visible_region_id": regionID,
"region_id": regionID,
"op_type": opType,
"coin_amount": amount,
}),
OccurredAtMS: occurredAt,
}
}
func mustJSON(t *testing.T, value map[string]any) string {
t.Helper()
body, err := json.Marshal(value)
if err != nil {
t.Fatalf("marshal json failed: %v", err)
}
return string(body)
}
func mustJSONNoT(value map[string]any) string {
body, err := json.Marshal(value)
if err != nil {
panic(err)
}
return string(body)
}