yumi-golang/internal/service/smashegg/flow_integration_test.go
2026-05-15 09:58:01 +08:00

235 lines
8.5 KiB
Go

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