467 lines
16 KiB
Go
467 lines
16 KiB
Go
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
|
|
}
|