package firstrechargereward import ( "context" "encoding/json" "testing" "chatapp3-golang/internal/config" "chatapp3-golang/internal/integration" "chatapp3-golang/internal/model" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func TestSaveConfigReturnsRewardItems(t *testing.T) { service, gateway, _ := newTestService(t) gateway.groups[1001] = testRewardGroup(1001, "首冲礼包") req := mustDecodeSaveConfigRequest(t, `{ "sysOrigin": "LIKEI", "enabled": true, "minAppVersion": "1.5.0", "levelConfigs": [ {"level": 1, "rechargeAmount": "9.99", "googleProductId": "gold_999", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true} ] }`) resp, err := service.SaveConfig(context.Background(), req) if err != nil { t.Fatalf("SaveConfig() error = %v", err) } if !resp.Configured || !resp.Enabled { t.Fatalf("SaveConfig() configured/enabled = %v/%v", resp.Configured, resp.Enabled) } if len(resp.LevelConfigs) != 1 { t.Fatalf("level count = %d", len(resp.LevelConfigs)) } level := resp.LevelConfigs[0] if level.RechargeAmountCents != 999 || level.RewardGroupID != 1001 { t.Fatalf("level payload = %+v", level) } if level.GoogleProductID != "gold_999" { t.Fatalf("google product id = %q", level.GoogleProductID) } if len(level.RewardItems) != 1 || level.RewardItems[0].Name != "首冲礼包奖励" { t.Fatalf("reward items = %+v", level.RewardItems) } } func TestSaveConfigReturnsVipCoverFromSourceURL(t *testing.T) { service, gateway, _ := newTestService(t) gateway.groups[2001] = integration.RewardGroupDetail{ ID: integration.Int64Value(2001), Name: "VIP礼包", RewardConfigList: []integration.RewardGroupItem{ { ID: integration.Int64Value(11), Type: "PROPS", DetailType: "NOBLE_VIP", Name: "VIP 1", Content: "2049402425177018400", Quantity: integration.Int64Value(30), Cover: "", SourceURL: "https://example.com/vip-short-badge.png", Remark: "30 天", }, }, } resp, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ "sysOrigin": "LIKEI", "enabled": true, "minAppVersion": "1.5.0", "levelConfigs": [ {"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "2001", "rewardGroupName": "VIP礼包", "enabled": true} ] }`)) if err != nil { t.Fatalf("SaveConfig() error = %v", err) } if len(resp.LevelConfigs) != 1 || len(resp.LevelConfigs[0].RewardItems) != 1 { t.Fatalf("reward items = %+v", resp.LevelConfigs) } if got := resp.LevelConfigs[0].RewardItems[0].Cover; got != "https://example.com/vip-short-badge.png" { t.Fatalf("vip cover = %q", got) } if got := resp.LevelConfigs[0].RewardItems[0].Type; got != "NOBLE_VIP" { t.Fatalf("vip type = %q", got) } } func TestRewardItemsReturnConcretePropsType(t *testing.T) { service, gateway, _ := newTestService(t) gateway.groups[3001] = integration.RewardGroupDetail{ ID: integration.Int64Value(3001), Name: "道具礼包", RewardConfigList: []integration.RewardGroupItem{ { ID: integration.Int64Value(21), Type: "PROPS", DetailType: "RIDE", Name: "Luxury sports car", Content: "2044620024357908482", Quantity: integration.Int64Value(7), Cover: "https://example.com/ride.png", Remark: "Luxury sports car", }, { ID: integration.Int64Value(22), Type: "PROPS", Name: "VIP 3", Content: "2050200000000000003", Quantity: integration.Int64Value(3), Cover: "https://example.com/vip3.png", Remark: "VIP 3", }, }, } resp, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ "sysOrigin": "LIKEI", "enabled": true, "minAppVersion": "1.5.0", "levelConfigs": [ {"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "3001", "rewardGroupName": "道具礼包", "enabled": true} ] }`)) if err != nil { t.Fatalf("SaveConfig() error = %v", err) } items := resp.LevelConfigs[0].RewardItems if len(items) != 2 { t.Fatalf("reward items = %+v", items) } if items[0].Type != "RIDE" { t.Fatalf("ride type = %q", items[0].Type) } if items[1].Type != "NOBLE_VIP" { t.Fatalf("vip fallback type = %q", items[1].Type) } } func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) { service, gateway, db := newTestService(t) gateway.groups[1001] = testRewardGroup(1001, "首冲礼包") gateway.profiles[123] = integration.UserProfile{ ID: integration.Int64Value(123), Account: "100123", UserNickname: "tester", CountryCode: "SA", } _, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ "sysOrigin": "LIKEI", "enabled": true, "minAppVersion": "1.5.0", "levelConfigs": [ {"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true} ] }`)) if err != nil { t.Fatalf("SaveConfig() error = %v", err) } unsupported, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ "eventId": "RECHARGE_SUCCESS:APPLE:1", "sysOrigin": "LIKEI", "userId": "123", "amountCents": "999", "payPlatform": "APPLE", "paymentMethod": "APPLE", "appVersion": "1.5.0", "sourceOrderId": "1" }`)) if err != nil { t.Fatalf("unsupported ProcessRechargeEvent() error = %v", err) } if unsupported.Processed || unsupported.Reason != "unsupported_payment_method" { t.Fatalf("unsupported response = %+v", unsupported) } lowAmount, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ "eventId": "RECHARGE_SUCCESS:GOOGLE:LOW", "sysOrigin": "LIKEI", "userId": "123", "amountCents": "499", "payPlatform": "GOOGLE", "paymentMethod": "GOOGLE", "appVersion": "1.5.0", "sourceOrderId": "LOW" }`)) if err != nil { t.Fatalf("low amount ProcessRechargeEvent() error = %v", err) } if lowAmount.Processed || lowAmount.Reason != "level_not_matched" { t.Fatalf("low amount response = %+v", lowAmount) } granted, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ "eventId": "RECHARGE_SUCCESS:GOOGLE:OK", "sysOrigin": "LIKEI", "userId": "123", "amountCents": "999", "payPlatform": "GOOGLE", "paymentMethod": "GOOGLE", "appVersion": "1.5.0", "sourceOrderId": "OK", "occurredAt": "2026-05-21T12:00:00Z" }`)) if err != nil { t.Fatalf("grant ProcessRechargeEvent() error = %v", err) } if !granted.Processed || granted.Status != statusSuccess || granted.Level != 1 { t.Fatalf("grant response = %+v", granted) } if len(gateway.rewardRequests) != 1 { t.Fatalf("reward request count = %d", len(gateway.rewardRequests)) } if got := gateway.rewardRequests[0].Origin; got != firstRechargeRewardOrigin { t.Fatalf("reward origin = %s", got) } if len(gateway.notices) != 1 || gateway.notices[0].NoticeType != firstRechargeRewardNoticeType { t.Fatalf("notices = %+v", gateway.notices) } duplicate, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ "eventId": "RECHARGE_SUCCESS:GOOGLE:OK", "sysOrigin": "LIKEI", "userId": "123", "amountCents": "999", "payPlatform": "GOOGLE", "paymentMethod": "GOOGLE", "appVersion": "1.5.0", "sourceOrderId": "OK" }`)) if err != nil { t.Fatalf("duplicate ProcessRechargeEvent() error = %v", err) } if !duplicate.Processed || duplicate.Status != statusSuccess { t.Fatalf("duplicate response = %+v", duplicate) } if len(gateway.rewardRequests) != 1 { t.Fatalf("duplicate reward request count = %d", len(gateway.rewardRequests)) } thirdPartyUnknownPlatform, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ "eventId": "RECHARGE_SUCCESS:THIRD_PARTY:CUSTOM", "sysOrigin": "LIKEI", "userId": "456", "amountCents": "999", "payPlatform": "CUSTOM_PAY", "paymentMethod": "THIRD_PARTY", "appVersion": "1.5.1", "sourceOrderId": "CUSTOM" }`)) if err != nil { t.Fatalf("third party ProcessRechargeEvent() error = %v", err) } if !thirdPartyUnknownPlatform.Processed || thirdPartyUnknownPlatform.Status != statusSuccess { t.Fatalf("third party response = %+v", thirdPartyUnknownPlatform) } if len(gateway.rewardRequests) != 2 { t.Fatalf("third party reward request count = %d", len(gateway.rewardRequests)) } googleDifferentPlatform, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ "eventId": "RECHARGE_SUCCESS:GOOGLE:CUSTOM", "sysOrigin": "LIKEI", "userId": "789", "amountCents": "999", "payPlatform": "CUSTOM_GOOGLE_GATEWAY", "paymentMethod": "GOOGLE", "appVersion": "1.5.1", "sourceOrderId": "CUSTOM_GOOGLE" }`)) if err != nil { t.Fatalf("google ProcessRechargeEvent() error = %v", err) } if !googleDifferentPlatform.Processed || googleDifferentPlatform.Status != statusSuccess { t.Fatalf("google response = %+v", googleDifferentPlatform) } if len(gateway.rewardRequests) != 3 { t.Fatalf("google reward request count = %d", len(gateway.rewardRequests)) } var count int64 if err := db.Model(&model.FirstRechargeRewardGrantRecord{}).Count(&count).Error; err != nil { t.Fatalf("count grant records: %v", err) } if count != 3 { t.Fatalf("grant record count = %d", count) } } func TestProcessRechargeEventStartsCountingAtSupportedAppVersion(t *testing.T) { service, gateway, db := newTestService(t) gateway.groups[1001] = testRewardGroup(1001, "首冲礼包") _, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ "sysOrigin": "LIKEI", "enabled": true, "minAppVersion": "1.5.0", "levelConfigs": [ {"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true} ] }`)) if err != nil { t.Fatalf("SaveConfig() error = %v", err) } oldVersion, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ "eventId": "RECHARGE_SUCCESS:GOOGLE:OLD_VERSION", "sysOrigin": "LIKEI", "userId": "123", "amountCents": "999", "payPlatform": "GOOGLE", "paymentMethod": "GOOGLE", "appVersion": "1.4.9", "sourceOrderId": "OLD_VERSION" }`)) if err != nil { t.Fatalf("old version ProcessRechargeEvent() error = %v", err) } if oldVersion.Processed || oldVersion.Reason != "app_version_not_supported" { t.Fatalf("old version response = %+v", oldVersion) } missingVersion, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ "eventId": "RECHARGE_SUCCESS:GOOGLE:MISSING_VERSION", "sysOrigin": "LIKEI", "userId": "123", "amountCents": "999", "payPlatform": "GOOGLE", "paymentMethod": "GOOGLE", "sourceOrderId": "MISSING_VERSION" }`)) if err != nil { t.Fatalf("missing version ProcessRechargeEvent() error = %v", err) } if missingVersion.Processed || missingVersion.Reason != "app_version_not_supported" { t.Fatalf("missing version response = %+v", missingVersion) } var count int64 if err := db.Model(&model.FirstRechargeRewardGrantRecord{}).Count(&count).Error; err != nil { t.Fatalf("count grant records after unsupported versions: %v", err) } if count != 0 { t.Fatalf("grant record count after unsupported versions = %d", count) } supported, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ "eventId": "RECHARGE_SUCCESS:GOOGLE:SUPPORTED_VERSION", "sysOrigin": "LIKEI", "userId": "123", "amountCents": "999", "payPlatform": "GOOGLE", "paymentMethod": "GOOGLE", "appVersion": "1.5.0", "sourceOrderId": "SUPPORTED_VERSION" }`)) if err != nil { t.Fatalf("supported version ProcessRechargeEvent() error = %v", err) } if !supported.Processed || supported.Status != statusSuccess { t.Fatalf("supported version response = %+v", supported) } if err := db.Model(&model.FirstRechargeRewardGrantRecord{}).Count(&count).Error; err != nil { t.Fatalf("count grant records after supported version: %v", err) } if count != 1 { t.Fatalf("grant record count after supported version = %d", count) } } func TestFirstRechargeRewardAppVersionSupported(t *testing.T) { tests := []struct { name string appVersion string want bool }{ {name: "missing", appVersion: "", want: false}, {name: "below", appVersion: "1.4.9", want: false}, {name: "equal", appVersion: "1.5.0", want: true}, {name: "short equal", appVersion: "1.5", want: true}, {name: "above", appVersion: "1.5.1", want: true}, {name: "suffix", appVersion: "1.5.0+100", want: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := isFirstRechargeRewardAppVersionSupported(tt.appVersion, "1.5.0"); got != tt.want { t.Fatalf("isFirstRechargeRewardAppVersionSupported(%q) = %v, want %v", tt.appVersion, got, tt.want) } }) } } func TestHomeRequiresConfiguredMinAppVersion(t *testing.T) { service, gateway, _ := newTestService(t) gateway.groups[1001] = testRewardGroup(1001, "首冲礼包") _, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ "sysOrigin": "LIKEI", "enabled": true, "minAppVersion": "2.0.0", "levelConfigs": [ {"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true} ] }`)) if err != nil { t.Fatalf("SaveConfig() error = %v", err) } oldVersion, err := service.GetHome(context.Background(), AuthUser{UserID: 123, SysOrigin: "LIKEI"}, "1.9.9") if err != nil { t.Fatalf("old version GetHome() error = %v", err) } if oldVersion.Enabled || oldVersion.ActivityStatus != activityStatusDisabled { t.Fatalf("old version home = %+v", oldVersion) } supported, err := service.GetHome(context.Background(), AuthUser{UserID: 123, SysOrigin: "LIKEI"}, "2.0.0") if err != nil { t.Fatalf("supported GetHome() error = %v", err) } if !supported.Enabled || supported.ActivityStatus != activityStatusOngoing { t.Fatalf("supported home = %+v", supported) } } func TestProcessRechargeEventMatchesGoogleProductIDBeforeAmount(t *testing.T) { service, gateway, _ := newTestService(t) gateway.groups[1001] = testRewardGroup(1001, "首冲礼包") gateway.groups[1002] = testRewardGroup(1002, "Google 首冲礼包") _, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ "sysOrigin": "LIKEI", "enabled": true, "minAppVersion": "1.5.0", "levelConfigs": [ {"level": 1, "rechargeAmount": "9.99", "googleProductId": "gold_999", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true}, {"level": 2, "rechargeAmount": "19.99", "googleProductId": "first_recharge_google_1999", "rewardGroupId": "1002", "rewardGroupName": "Google 首冲礼包", "enabled": true} ] }`)) if err != nil { t.Fatalf("SaveConfig() error = %v", err) } granted, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ "eventId": "RECHARGE_SUCCESS:GOOGLE:PRODUCT_ID", "sysOrigin": "LIKEI", "userId": "123", "amountCents": "999", "payPlatform": "GOOGLE", "paymentMethod": "GOOGLE", "googleProductId": "first_recharge_google_1999", "appVersion": "1.5.0", "sourceOrderId": "PRODUCT_ID" }`)) if err != nil { t.Fatalf("product id ProcessRechargeEvent() error = %v", err) } if !granted.Processed || granted.Status != statusSuccess || granted.Level != 2 { t.Fatalf("product id response = %+v", granted) } if len(gateway.rewardRequests) != 1 || gateway.rewardRequests[0].SourceGroupID != 1002 { t.Fatalf("reward requests = %+v", gateway.rewardRequests) } } func newTestService(t *testing.T) (*Service, *fakeGateway, *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.FirstRechargeRewardConfig{}, &model.FirstRechargeRewardLevel{}, &model.FirstRechargeRewardGrantRecord{}, ); err != nil { t.Fatalf("migrate sqlite: %v", err) } gateway := &fakeGateway{ groups: map[int64]integration.RewardGroupDetail{}, profiles: map[int64]integration.UserProfile{}, } service := NewService(config.Config{ FirstRechargeReward: config.FirstRechargeRewardConfig{ DefaultSysOrigin: "LIKEI", }, }, db, gateway) return service, gateway, db } func mustDecodeSaveConfigRequest(t *testing.T, raw string) SaveConfigRequest { t.Helper() var req SaveConfigRequest if err := json.Unmarshal([]byte(raw), &req); err != nil { t.Fatalf("decode SaveConfigRequest: %v", err) } return req } func mustDecodeRechargeEvent(t *testing.T, raw string) RechargeEvent { t.Helper() var event RechargeEvent if err := json.Unmarshal([]byte(raw), &event); err != nil { t.Fatalf("decode RechargeEvent: %v", err) } return event } func testRewardGroup(id int64, name string) integration.RewardGroupDetail { return integration.RewardGroupDetail{ ID: integration.Int64Value(id), Name: name, RewardConfigList: []integration.RewardGroupItem{ { ID: integration.Int64Value(11), Type: "PROPS", DetailType: "RIDE", Name: name + "奖励", Content: "10001", Quantity: integration.Int64Value(1), Cover: "https://example.com/reward.png", }, }, } } type fakeGateway struct { groups map[int64]integration.RewardGroupDetail profiles map[int64]integration.UserProfile rewardRequests []integration.SendActivityRewardRequest notices []integration.OfficialNoticeCustomizeRequest cacheRemoved []int64 } func (g *fakeGateway) GetRewardGroupDetail(_ context.Context, groupID int64) (integration.RewardGroupDetail, error) { if group, ok := g.groups[groupID]; ok { return group, nil } return testRewardGroup(groupID, "默认奖励组"), nil } func (g *fakeGateway) GetUserProfile(_ context.Context, userID int64) (integration.UserProfile, error) { if profile, ok := g.profiles[userID]; ok { return profile, nil } return integration.UserProfile{ID: integration.Int64Value(userID)}, nil } func (g *fakeGateway) SendActivityReward(_ context.Context, req integration.SendActivityRewardRequest) error { g.rewardRequests = append(g.rewardRequests, req) return nil } func (g *fakeGateway) SendOfficialNoticeCustomize(_ context.Context, req integration.OfficialNoticeCustomizeRequest) error { g.notices = append(g.notices, req) return nil } func (g *fakeGateway) RemoveUserProfileCacheAll(_ context.Context, userID int64) error { g.cacheRemoved = append(g.cacheRemoved, userID) return nil }