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