//go:build integration package smashegg import ( "context" "fmt" "os" "strconv" "strings" "testing" "chatapp3-golang/internal/config" "chatapp3-golang/internal/integration" "chatapp3-golang/internal/repo" ) type realResourceRow struct { ID int64 `gorm:"column:id"` Type string `gorm:"column:type"` Name string `gorm:"column:name"` Cover string `gorm:"column:cover"` SourceURL string `gorm:"column:source_url"` AmountGold int64 `gorm:"column:amount_gold"` } func TestRealDataAdminConfigThenDraw(t *testing.T) { ctx := context.Background() cfg := config.Load() repository, err := repo.New(cfg) if err != nil { t.Fatalf("init repository: %v", err) } if err := repository.Ping(ctx); err != nil { t.Fatalf("ping real dependencies: %v", err) } gateways := integration.NewGateways(cfg) service := NewService(cfg, repository.DB, &gateways) sysOrigin := "LIKEI" userID := realFlowUserID() resource := firstRealResource(t, ctx, repository, sysOrigin) req := SaveConfigRequest{ SysOrigin: sysOrigin, Enabled: true, Timezone: "Asia/Riyadh", RTPBasisPoints: 9500, NewbiePoolEnabled: true, NewbieWindowDays: 90, NewbieMaxDrawCount: 1000, NewbieMinRechargeAmount: 0, DrawOptions: []DrawOptionInput{ {Enabled: true, Sort: 1, OptionKey: optionKeyOne, Label: "Smash 1", Times: 1, PriceGold: 500}, {Enabled: true, Sort: 2, OptionKey: optionKeyTen, Label: "Smash 10", Times: 10, PriceGold: 5000}, {Enabled: true, Sort: 3, OptionKey: optionKeyHundred, Label: "Smash 100", Times: 100, PriceGold: 50000}, }, Rewards: []RewardConfigInput{ {Enabled: true, Sort: 1, RewardType: rewardTypeGold, GoldAmount: 50}, {Enabled: true, Sort: 2, RewardType: rewardTypeGold, GoldAmount: 100}, {Enabled: true, Sort: 3, RewardType: rewardTypeGold, GoldAmount: 500}, { Enabled: true, Sort: 4, RewardType: rewardTypeResource, ResourceID: ResourceID(resource.ID), ResourceType: resource.Type, ResourceName: resource.Name, ResourceURL: resource.SourceURL, CoverURL: resource.Cover, AnimationURL: resource.SourceURL, DurationDays: 1, DisplayGoldAmount: resource.AmountGold, }, }, NewbieRewards: []RewardConfigInput{ {Enabled: true, Sort: 1, RewardType: rewardTypeGold, GoldAmount: 400}, {Enabled: true, Sort: 2, RewardType: rewardTypeGold, GoldAmount: 500}, {Enabled: true, Sort: 3, RewardType: rewardTypeGold, GoldAmount: 600}, }, } configResp, err := service.SaveConfig(ctx, req) if err != nil { t.Fatalf("save admin config: %v", err) } if configResp.RTPBasisPoints != 9500 || len(configResp.DrawOptions) != 3 { t.Fatalf("unexpected config response: rtp=%d options=%d", configResp.RTPBasisPoints, len(configResp.DrawOptions)) } assertProbabilityTotal(t, "general", configResp.Rewards) assertProbabilityTotal(t, "newbie", configResp.NewbieRewards) assertResourceRewardPresent(t, configResp.Rewards, resource.ID) t.Logf("admin_config saved sysOrigin=%s rtp=%s options=%d generalRewards=%d newbieRewards=%d resource=%s:%d price=%d", configResp.SysOrigin, configResp.RTPPercent, len(configResp.DrawOptions), len(configResp.Rewards), len(configResp.NewbieRewards), resource.Type, resource.ID, resource.AmountGold) appConfig, err := service.GetAppConfig(ctx, AuthUser{UserID: userID, SysOrigin: sysOrigin}) if err != nil { t.Fatalf("get app config: %v", err) } if appConfig.ActivePoolType != poolTypeNewbie || !appConfig.NewbieEligible { t.Fatalf("expected newbie pool, got pool=%s eligible=%v remaining=%d", appConfig.ActivePoolType, appConfig.NewbieEligible, appConfig.NewbieRemainingDrawCount) } t.Logf("app_config activePool=%s newbieRemaining=%d drawOptions=%d", appConfig.ActivePoolType, appConfig.NewbieRemainingDrawCount, len(appConfig.DrawOptions)) balanceBefore, err := goldBalance(ctx, &gateways, userID) if err != nil { t.Fatalf("balance before: %v", err) } if balanceBefore < 5000 { t.Fatalf("test user balance=%d is below draw price", balanceBefore) } var recordsBefore int64 if err := repository.DB.WithContext(ctx).Table("smash_egg_draw_record").Where("sys_origin = ? AND user_id = ?", sysOrigin, userID).Count(&recordsBefore).Error; err != nil { t.Fatalf("count records before: %v", err) } drawResp, err := service.Draw(ctx, AuthUser{UserID: userID, SysOrigin: sysOrigin}, DrawRequest{Times: 10}) if err != nil { t.Fatalf("draw: %v", err) } if drawResp.DrawTimes != 10 || drawResp.PaidGold != 5000 || drawResp.PoolType != poolTypeNewbie { t.Fatalf("unexpected draw response: times=%d paid=%d pool=%s", drawResp.DrawTimes, drawResp.PaidGold, drawResp.PoolType) } if len(drawResp.Records) != 10 || len(drawResp.Rewards) == 0 { t.Fatalf("unexpected reward records: records=%d rewards=%d", len(drawResp.Records), len(drawResp.Rewards)) } for _, record := range drawResp.Records { if record.Status != drawStatusSuccess { t.Fatalf("record %d status=%s error=%s", record.ID, record.Status, record.ErrorMessage) } } balanceAfter, err := goldBalance(ctx, &gateways, userID) if err != nil { t.Fatalf("balance after: %v", err) } expectedBalanceAfter := balanceBefore - drawResp.PaidGold + drawResp.RewardValue if balanceAfter != expectedBalanceAfter { t.Fatalf("balance after=%d, want %d (before=%d paid=%d reward=%d)", balanceAfter, expectedBalanceAfter, balanceBefore, drawResp.PaidGold, drawResp.RewardValue) } var recordsAfter int64 if err := repository.DB.WithContext(ctx).Table("smash_egg_draw_record").Where("sys_origin = ? AND user_id = ?", sysOrigin, userID).Count(&recordsAfter).Error; err != nil { t.Fatalf("count records after: %v", err) } if recordsAfter-recordsBefore != int64(drawResp.DrawTimes) { t.Fatalf("record delta=%d, want %d", recordsAfter-recordsBefore, drawResp.DrawTimes) } t.Logf("draw_success drawNo=%s paid=%d reward=%d balanceBefore=%d balanceAfter=%d recordsDelta=%d", drawResp.DrawNo, drawResp.PaidGold, drawResp.RewardValue, balanceBefore, balanceAfter, recordsAfter-recordsBefore) adminRecords, err := service.PageAdminRecords(ctx, sysOrigin, userID, drawStatusSuccess, 1, 20, "", "") if err != nil { t.Fatalf("page admin records: %v", err) } if len(adminRecords.Records) == 0 || adminRecords.Records[0].DrawNo == "" { t.Fatalf("admin records are empty after draw") } t.Logf("admin_records total=%d firstDrawNo=%s firstReward=%s", adminRecords.Total, adminRecords.Records[0].DrawNo, adminRecords.Records[0].RewardGroupName) } func realFlowUserID() int64 { raw := strings.TrimSpace(os.Getenv("SMASH_EGG_REAL_FLOW_USER_ID")) if raw == "" { return 2052720059744202753 } parsed, err := strconv.ParseInt(raw, 10, 64) if err != nil || parsed <= 0 { return 2052720059744202753 } return parsed } func firstRealResource(t *testing.T, ctx context.Context, repository *repo.Repository, sysOrigin string) realResourceRow { t.Helper() var row realResourceRow err := repository.DB.WithContext(ctx).Raw( `SELECT id, type, name, cover, source_url, CAST(amount AS SIGNED) AS amount_gold FROM props_source_record WHERE sys_origin = ? AND is_del = 0 AND amount > 0 AND amount < 300000 ORDER BY amount ASC, id ASC LIMIT 1`, sysOrigin, ).Scan(&row).Error if err != nil { t.Fatalf("query real resource: %v", err) } if row.ID <= 0 { t.Fatalf("no real resource found for sysOrigin=%s", sysOrigin) } if strings.TrimSpace(row.Type) == "" { t.Fatalf("real resource %d has empty type", row.ID) } if row.AmountGold <= 0 { row.AmountGold = 5000 } return row } func assertProbabilityTotal(t *testing.T, name string, rewards []RewardConfigPayload) { t.Helper() total := 0 for _, reward := range rewards { if reward.Enabled { total += reward.Probability } } if total != probabilityTotal { t.Fatalf("%s probability total=%d, want %d", name, total, probabilityTotal) } } func assertResourceRewardPresent(t *testing.T, rewards []RewardConfigPayload, resourceID int64) { t.Helper() for _, reward := range rewards { if reward.RewardType == rewardTypeResource && reward.ResourceID.Int64() == resourceID && reward.DisplayGoldAmount > 0 { return } } t.Fatalf("resource reward %d with display price was not found", resourceID) } func goldBalance(ctx context.Context, gateways *integration.Gateways, userID int64) (int64, error) { balances, err := gateways.MapGoldBalance(ctx, []int64{userID}) if err != nil { return 0, err } value, ok := balances[userID] if !ok { return 0, fmt.Errorf("missing balance for user %d", userID) } return value, nil }