package rechargereward import ( "context" "encoding/json" "fmt" "strings" "testing" "time" "chatapp3-golang/internal/config" "chatapp3-golang/internal/integration" "chatapp3-golang/internal/model" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func newTestService(t *testing.T, java rechargeRewardJavaGateway) (*Service, *gorm.DB) { t.Helper() db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate( &model.RechargeRewardConfig{}, &model.RechargeRewardLevel{}, &model.RechargeRewardGrantRecord{}, &model.UserBaseInfo{}, ); err != nil { t.Fatalf("auto migrate: %v", err) } if err := db.Exec(`ALTER TABLE user_base_info ADD COLUMN is_del INTEGER DEFAULT 0`).Error; err != nil { t.Fatalf("add is_del: %v", err) } service := NewService(config.Config{ RechargeReward: config.RechargeRewardConfig{ DefaultSysOrigin: "LIKEI", Timezone: "Asia/Riyadh", StorageTimezone: "Asia/Shanghai", StartDate: "2020-01-01", CoinSellerGoldPerUSD: 100000, }, }, db, java) return service, db } func TestGetConfigReturnsEmptyDraftWhenMissing(t *testing.T) { service, _ := newTestService(t, nil) resp, err := service.GetConfig(context.Background(), "") if err != nil { t.Fatalf("GetConfig() error = %v", err) } if resp.Configured || resp.Enabled { t.Fatalf("GetConfig() configured/enabled = %v/%v, want false/false", resp.Configured, resp.Enabled) } if resp.SysOrigin != "LIKEI" || resp.Timezone != "Asia/Riyadh" { t.Fatalf("GetConfig() sys/timezone = %q/%q", resp.SysOrigin, resp.Timezone) } if len(resp.LevelConfigs) != 0 { t.Fatalf("default level count = %d, want 0", len(resp.LevelConfigs)) } } func TestSaveConfigRejectsDuplicateRechargeAmount(t *testing.T) { service, _ := newTestService(t, nil) _, err := service.SaveConfig(context.Background(), SaveRechargeRewardConfigRequest{ SysOrigin: "LIKEI", Enabled: true, Timezone: "Asia/Riyadh", LevelConfigs: []RechargeRewardLevelInput{ {Level: 1, RechargeAmount: mustAmount(t, "100"), RewardGold: 1000, Enabled: true}, {Level: 2, RechargeAmount: mustAmount(t, "100.00"), RewardGold: 2000, Enabled: true}, }, }) if err == nil { t.Fatalf("SaveConfig() error = nil, want duplicate amount error") } } func TestSaveConfigUpsertsBySysOrigin(t *testing.T) { service, _ := newTestService(t, nil) ctx := context.Background() first, err := service.SaveConfig(ctx, SaveRechargeRewardConfigRequest{ SysOrigin: "likei", Enabled: true, Timezone: "Asia/Riyadh", LevelConfigs: []RechargeRewardLevelInput{ {Level: 1, RechargeAmount: mustAmount(t, "100"), RewardGold: 1000, Enabled: true}, }, }) if err != nil { t.Fatalf("first SaveConfig() error = %v", err) } second, err := service.SaveConfig(ctx, SaveRechargeRewardConfigRequest{ SysOrigin: "LIKEI", Enabled: true, Timezone: "Asia/Riyadh", LevelConfigs: []RechargeRewardLevelInput{ {Level: 1, RechargeAmount: mustAmount(t, "200.50"), RewardGold: 3000, Enabled: true}, }, }) if err != nil { t.Fatalf("second SaveConfig() error = %v", err) } if first.ID != second.ID { t.Fatalf("config ID changed on upsert: first=%d second=%d", first.ID, second.ID) } if got := second.LevelConfigs[0]; got.RechargeAmount != "200.50" || got.RewardGold != 3000 { t.Fatalf("upserted level = %+v", got) } } func TestGetHomeUsesJavaRechargeAndMarksLevels(t *testing.T) { groupID := int64(2056981527522242600) java := rechargeRewardTestGateway{ total: map[int64][]integration.UserTotalRecharge{ 42: { {UserID: integration.Int64Value(42), Type: "GOOGLE", Amount: integration.DecimalString("5000.00")}, {UserID: integration.Int64Value(42), Type: "APPLE", Amount: integration.DecimalString("998.00")}, }, }, profile: integration.UserProfile{ ID: integration.Int64Value(42), Account: "12345678", UserNickname: "nanfangjian", UserAvatar: "https://example.com/avatar.png", }, groups: map[int64]integration.RewardGroupDetail{ groupID: { ID: integration.Int64Value(groupID), Name: "Gift Box", RewardConfigList: []integration.RewardGroupItem{ { ID: integration.Int64Value(1), Type: "PROPS", DetailType: "AVATAR_FRAME", Name: "Frame", Quantity: integration.Int64Value(1), SourceURL: "https://example.com/frame.svga", }, }, }, }, } service, db := newTestService(t, java) createRechargeRewardSourceTables(t, db) if err := db.Create(&model.UserBaseInfo{ ID: 42, Account: "1000042", UserNickname: "Nina", OriginSys: "LIKEI", CountryCode: "SA", CountryName: "Saudi Arabia", }).Error; err != nil { t.Fatalf("create user: %v", err) } rechargeAt := rechargeRewardStorageTime(time.Now().In(service.location), service.storageLocation) if err := db.Exec(` INSERT INTO order_purchase_history (user_id, sys_origin, evn, is_trial_period, status, pay_platform, unit_price, create_time) VALUES (42, 'LIKEI', 'PROD', 0, 'COMPLETE', 'GOOGLE', 120.50, ?) `, rechargeAt).Error; err != nil { t.Fatalf("insert google recharge: %v", err) } if err := db.Exec(` INSERT INTO order_user_purchase_pay (user_id, sys_origin, evn, factory_code, pay_status, receipt_type, refund_status, compute_usd_amount, create_time, update_time) VALUES (42, 'LIKEI', 'PROD', 'MIFA_PAY', 'SUCCESSFUL', 'PAYMENT', 'NONE', 29.00, ?, ?) `, rechargeAt, rechargeAt).Error; err != nil { t.Fatalf("insert mifapay recharge: %v", err) } if err := db.Exec(` INSERT INTO likei_wallet.user_freight_balance_running_water (accept_user_id, sys_origin, origin, type, quantity, create_time) VALUES (42, 'LIKEI', 'SHIPMENT', 1, 100000, ?) `, rechargeAt).Error; err != nil { t.Fatalf("insert seller recharge: %v", err) } if _, err := service.SaveConfig(context.Background(), SaveRechargeRewardConfigRequest{ SysOrigin: "LIKEI", Enabled: true, Timezone: "Asia/Riyadh", LevelConfigs: []RechargeRewardLevelInput{ {Level: 1, RechargeAmount: mustAmount(t, "100"), RewardGroupID: ptrFlexibleInt64(groupID), Enabled: true}, {Level: 2, RechargeAmount: mustAmount(t, "200"), RewardGold: 3000, Enabled: true}, }, }); err != nil { t.Fatalf("SaveConfig() error = %v", err) } resp, err := service.GetHome(context.Background(), AuthUser{UserID: 42, SysOrigin: "LIKEI"}) if err != nil { t.Fatalf("GetHome() error = %v", err) } if resp.CurrentRechargeAmount != "150.50" || resp.AccumulatedRechargeAmount != "5998.00" { t.Fatalf("amounts = current %q accumulated %q", resp.CurrentRechargeAmount, resp.AccumulatedRechargeAmount) } if resp.MatchedLevel != 1 || resp.NextLevel != 2 { t.Fatalf("levels = matched %d next %d", resp.MatchedLevel, resp.NextLevel) } if len(resp.LevelConfigs) != 2 || !resp.LevelConfigs[0].Reached || resp.LevelConfigs[1].Reached { t.Fatalf("level reach state = %+v", resp.LevelConfigs) } if len(resp.LevelConfigs[0].RewardItems) != 1 || resp.LevelConfigs[0].RewardItems[0].Name != "Frame" { t.Fatalf("reward items = %+v", resp.LevelConfigs[0].RewardItems) } if got := resp.LevelConfigs[0].RewardItems[0]; got.Type != "AVATAR_FRAME" || got.Cover != "https://example.com/frame.svga" { t.Fatalf("reward item display fields = %+v", got) } if resp.LevelConfigs[1].RewardItems == nil { t.Fatalf("gold-only level RewardItems = nil, want empty slice") } encoded, err := json.Marshal(resp) if err != nil { t.Fatalf("marshal response: %v", err) } body := string(encoded) if !strings.Contains(body, `"rewardGroupId":"2056981527522242600"`) { t.Fatalf("encoded rewardGroupId is not a string: %s", body) } if strings.Contains(body, `"rewardItems":null`) { t.Fatalf("encoded rewardItems contains null: %s", body) } } func TestGetHomeFallsBackToRewardGroupNameWhenStoredIDLostPrecision(t *testing.T) { actualGroupID := int64(2056723577461338114) roundedGroupID := int64(2056723577461338000) service, _ := newTestService(t, rechargeRewardTestGateway{ groups: map[int64]integration.RewardGroupDetail{ actualGroupID: { ID: integration.Int64Value(actualGroupID), Name: "累充活动$50", RewardConfigList: []integration.RewardGroupItem{ {ID: integration.Int64Value(1), Type: "BADGE", Name: "recharge50$", Quantity: integration.Int64Value(30), Cover: "https://example.com/badge.png"}, }, }, }, }) if _, err := service.SaveConfig(context.Background(), SaveRechargeRewardConfigRequest{ SysOrigin: "LIKEI", Enabled: true, Timezone: "Asia/Riyadh", LevelConfigs: []RechargeRewardLevelInput{ {Level: 1, RechargeAmount: mustAmount(t, "50"), RewardGroupID: ptrFlexibleInt64(roundedGroupID), RewardGroupName: "累充活动$50", Enabled: true}, }, }); err != nil { t.Fatalf("SaveConfig() error = %v", err) } resp, err := service.GetConfig(context.Background(), "LIKEI") if err != nil { t.Fatalf("GetConfig() error = %v", err) } if len(resp.LevelConfigs) != 1 || len(resp.LevelConfigs[0].RewardItems) != 1 { t.Fatalf("reward items = %+v, want fallback by reward group name", resp.LevelConfigs) } if resp.LevelConfigs[0].RewardGroupID == nil || *resp.LevelConfigs[0].RewardGroupID != actualGroupID { t.Fatalf("rewardGroupId = %v, want resolved actual group id %d", resp.LevelConfigs[0].RewardGroupID, actualGroupID) } if got := resp.LevelConfigs[0].RewardItems[0].Name; got != "recharge50$" { t.Fatalf("reward item name = %q, want recharge50$", got) } } func TestPageClaimRecordsUsesActualReceiveRecords(t *testing.T) { groupID := int64(9001) service, db := newTestService(t, rechargeRewardTestGateway{ groups: map[int64]integration.RewardGroupDetail{ groupID: { ID: integration.Int64Value(groupID), Name: "Gift Box", RewardConfigList: []integration.RewardGroupItem{ {ID: integration.Int64Value(1), Type: "PROP", Name: "Frame", Quantity: integration.Int64Value(1)}, }, }, }, }) if _, err := service.SaveConfig(context.Background(), SaveRechargeRewardConfigRequest{ SysOrigin: "LIKEI", Enabled: true, Timezone: "Asia/Riyadh", LevelConfigs: []RechargeRewardLevelInput{ {Level: 1, RechargeAmount: mustAmount(t, "100"), RewardGroupID: ptrFlexibleInt64(groupID), Enabled: true}, {Level: 2, RechargeAmount: mustAmount(t, "200"), RewardGold: 3000, Enabled: true}, }, }); err != nil { t.Fatalf("SaveConfig() error = %v", err) } if err := db.Create(&model.UserBaseInfo{ ID: 42, Account: "1000042", UserNickname: "Nina", OriginSys: "LIKEI", CountryCode: "SA", CountryName: "Saudi Arabia", }).Error; err != nil { t.Fatalf("create user: %v", err) } if err := db.Create(&model.RechargeRewardGrantRecord{ ID: 8001, ConfigID: 1001, LevelID: 7001, SysOrigin: "LIKEI", CycleKey: "20260521", RechargeDate: 202605, PeriodStartAt: time.Date(2026, 5, 21, 0, 0, 0, 0, time.UTC), PeriodEndAt: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC), UserID: 42, Account: "1000042", UserNickname: "Nina", CountryCode: "SA", CountryName: "Saudi Arabia", RechargeAmountCents: 15050, Level: 1, RechargeThresholdCents: 10000, RewardGroupID: &groupID, RewardGroupName: "Gift Box", Status: "SUCCESS", RewardGroupStatus: "SUCCESS", RewardGoldStatus: "SKIPPED", SentAt: ptrTime(time.Date(2026, 5, 21, 10, 30, 0, 0, time.UTC)), CreateTime: time.Date(2026, 5, 21, 10, 30, 0, 0, time.UTC), UpdateTime: time.Date(2026, 5, 21, 10, 30, 0, 0, time.UTC), }).Error; err != nil { t.Fatalf("insert grant record: %v", err) } resp, err := service.PageClaimRecords(context.Background(), "LIKEI", "2026-05", 0, 1, 20) if err != nil { t.Fatalf("PageClaimRecords() error = %v", err) } if resp.Total != 1 || len(resp.Records) != 1 { t.Fatalf("page result total=%d records=%d", resp.Total, len(resp.Records)) } record := resp.Records[0] if record.RechargeAmount != "150.50" { t.Fatalf("RechargeAmount = %q, want 150.50", record.RechargeAmount) } if len(record.ReachedLevels) != 1 || record.ReachedLevels[0].Level != 1 { t.Fatalf("ReachedLevels = %+v, want only level 1", record.ReachedLevels) } if record.ReachedLevels[0].RechargeAmount != "100.00" { t.Fatalf("ReachedLevels[0].RechargeAmount = %q, want 100.00", record.ReachedLevels[0].RechargeAmount) } if len(record.ClaimedRewards) != 1 || len(record.ClaimedRewards[0].RewardItems) != 1 { t.Fatalf("ClaimedRewards = %+v, want level reward item", record.ClaimedRewards) } } func TestParseAmountCents(t *testing.T) { tests := map[string]int64{ "100": 10000, "100.5": 10050, "100.50": 10050, "5,998.00": 599800, } for input, want := range tests { got, err := parseAmountCents(input) if err != nil { t.Fatalf("parseAmountCents(%q) error = %v", input, err) } if got != want { t.Fatalf("parseAmountCents(%q) = %d, want %d", input, got, want) } } } func mustAmount(t *testing.T, raw string) flexibleAmount { t.Helper() cents, err := parseAmountCents(raw) if err != nil { t.Fatalf("parse amount %q: %v", raw, err) } return flexibleAmount(cents) } func ptrFlexibleInt64(value int64) flexibleOptionalInt64 { return flexibleOptionalInt64{value: &value} } func ptrTime(value time.Time) *time.Time { return &value } func createRechargeRewardSourceTables(t *testing.T, db *gorm.DB) { t.Helper() if err := db.Exec(`ATTACH DATABASE ':memory:' AS likei_wallet`).Error; err != nil { t.Fatalf("attach wallet schema: %v", err) } if err := db.Exec(` CREATE TABLE order_purchase_history ( user_id INTEGER NOT NULL, sys_origin TEXT NOT NULL, evn TEXT NOT NULL, is_trial_period INTEGER NOT NULL, status TEXT NOT NULL, pay_platform TEXT NOT NULL, unit_price DECIMAL(12,2) NOT NULL, create_time DATETIME NOT NULL )`).Error; err != nil { t.Fatalf("create order_purchase_history: %v", err) } if err := db.Exec(` CREATE TABLE order_user_purchase_pay ( user_id INTEGER NOT NULL, sys_origin TEXT NOT NULL, evn TEXT NOT NULL, factory_code TEXT NOT NULL, pay_status TEXT NOT NULL, receipt_type TEXT NOT NULL, refund_status TEXT NOT NULL, compute_usd_amount DECIMAL(12,2) NOT NULL, create_time DATETIME NOT NULL, update_time DATETIME NOT NULL )`).Error; err != nil { t.Fatalf("create order_user_purchase_pay: %v", err) } if err := db.Exec(` CREATE TABLE likei_wallet.user_freight_balance_running_water ( accept_user_id INTEGER NOT NULL, sys_origin TEXT NOT NULL, origin TEXT NOT NULL, type INTEGER NOT NULL, quantity DECIMAL(20,2) NOT NULL, create_time DATETIME NOT NULL )`).Error; err != nil { t.Fatalf("create wallet freight water: %v", err) } } type rechargeRewardTestGateway struct { monthly integration.DecimalString total map[int64][]integration.UserTotalRecharge profile integration.UserProfile groups map[int64]integration.RewardGroupDetail } func (g rechargeRewardTestGateway) GetUserProfile(context.Context, int64) (integration.UserProfile, error) { return g.profile, nil } func (g rechargeRewardTestGateway) GetRewardGroupDetail(_ context.Context, groupID int64) (integration.RewardGroupDetail, error) { if detail, ok := g.groups[groupID]; ok { return detail, nil } return integration.RewardGroupDetail{}, fmt.Errorf("group not found") } func (g rechargeRewardTestGateway) FindRewardGroupDetail(_ context.Context, _ string, name string) (integration.RewardGroupDetail, error) { for _, detail := range g.groups { if detail.Name == name { return detail, nil } } return integration.RewardGroupDetail{}, fmt.Errorf("group not found") } func (g rechargeRewardTestGateway) MapUserTotalRecharge(context.Context, []int64) (map[int64][]integration.UserTotalRecharge, error) { return g.total, nil } func (g rechargeRewardTestGateway) GetThisMonthTotalPersonalRecharge(context.Context, int64) (integration.DecimalString, error) { return g.monthly, nil }