package activity import ( "context" "testing" "time" "bindbox-game/internal/repository/mysql/dao" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func newRewardSnapshotTestService(t *testing.T) (*service, *dao.Query, *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 failed: %v", err) } if err := db.Exec(`CREATE TABLE products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, price INTEGER NOT NULL, cost_price INTEGER NOT NULL DEFAULT 0, stock INTEGER NOT NULL, images_json TEXT, updated_at DATETIME, deleted_at DATETIME );`).Error; err != nil { t.Fatalf("create products failed: %v", err) } if err := db.Exec(`CREATE TABLE activities ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, name TEXT, banner TEXT, activity_category_id INTEGER, status INTEGER, price_draw INTEGER, is_boss INTEGER, allow_item_cards BOOLEAN, allow_coupons BOOLEAN, end_time DATETIME, start_time DATETIME, scheduled_time DATETIME, last_settled_at DATETIME, draw_mode TEXT, play_type TEXT, min_participants INTEGER, interval_minutes INTEGER, refund_coupon_id INTEGER, deleted_at DATETIME, image TEXT, commitment_algo TEXT, commitment_seed_master TEXT, commitment_seed_hash TEXT, commitment_state_version INTEGER, commitment_items_root TEXT, gameplay_intro TEXT, daily_seed TEXT, daily_seed_date TEXT, last_daily_seed TEXT, last_daily_seed_date TEXT );`).Error; err != nil { t.Fatalf("create activities failed: %v", err) } if err := db.Exec(`CREATE TABLE activity_issues ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, activity_id INTEGER NOT NULL, issue_number TEXT, status INTEGER, sort INTEGER, deleted_at DATETIME );`).Error; err != nil { t.Fatalf("create activity_issues failed: %v", err) } if err := db.Exec(`CREATE TABLE activity_reward_settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME, updated_at DATETIME, issue_id INTEGER NOT NULL, product_id INTEGER, price_snapshot_cents INTEGER NOT NULL DEFAULT 0, price_snapshot_at DATETIME, weight INTEGER NOT NULL, quantity INTEGER NOT NULL, original_qty INTEGER NOT NULL, level INTEGER NOT NULL, sort INTEGER, is_boss INTEGER, min_score INTEGER NOT NULL DEFAULT 0, drop_quantity INTEGER NOT NULL DEFAULT 1, cost_snapshot_cents INTEGER NOT NULL DEFAULT 0, deleted_at DATETIME );`).Error; err != nil { t.Fatalf("create activity_reward_settings failed: %v", err) } q := dao.Use(db) svc := &service{readDB: q, writeDB: q, repo: nil} return svc, q, db } func TestCreateIssueRewards_SnapshotFromProductPrice(t *testing.T) { svc, q, db := newRewardSnapshotTestService(t) ctx := context.Background() if err := db.Exec("INSERT INTO products (id, name, price, cost_price, stock, images_json) VALUES (101, 'A', 1000, 500, 10, '[]')").Error; err != nil { t.Fatalf("insert product failed: %v", err) } err := svc.CreateIssueRewards(ctx, 88, []CreateRewardInput{ { ProductID: 101, Weight: 1, Quantity: 2, OriginalQty: 2, Level: 1, Sort: 1, IsBoss: 0, MinScore: 0, }, }) if err != nil { t.Fatalf("CreateIssueRewards failed: %v", err) } row, err := q.ActivityRewardSettings.WithContext(ctx).Where(q.ActivityRewardSettings.IssueID.Eq(88)).First() if err != nil { t.Fatalf("query reward failed: %v", err) } if row.PriceSnapshotCents != 1000 { t.Fatalf("expected snapshot=1000, got=%d", row.PriceSnapshotCents) } if row.CostSnapshotCents != 500 { t.Fatalf("expected cost snapshot=500, got=%d", row.CostSnapshotCents) } if row.PriceSnapshotAt.IsZero() { t.Fatalf("expected price snapshot time to be set") } } func TestModifyIssueReward_ProductChanged_RecomputeSnapshot(t *testing.T) { svc, q, db := newRewardSnapshotTestService(t) ctx := context.Background() _ = db.Exec("INSERT INTO products (id, name, price, cost_price, stock, images_json) VALUES (101, 'A', 1000, 500, 10, '[]')").Error _ = db.Exec("INSERT INTO products (id, name, price, cost_price, stock, images_json) VALUES (102, 'B', 2300, 1300, 10, '[]')").Error _ = db.Exec("INSERT INTO activity_reward_settings (id, issue_id, product_id, price_snapshot_cents, cost_snapshot_cents, price_snapshot_at, weight, quantity, original_qty, level, sort, is_boss, min_score, drop_quantity) VALUES (1, 9, 101, 1000, 500, CURRENT_TIMESTAMP, 1, 1, 1, 1, 1, 0, 0, 1)").Error newProductID := int64(102) if err := svc.ModifyIssueReward(ctx, 1, ModifyRewardInput{ProductID: &newProductID}); err != nil { t.Fatalf("ModifyIssueReward failed: %v", err) } row, err := q.ActivityRewardSettings.WithContext(ctx).Where(q.ActivityRewardSettings.ID.Eq(1)).First() if err != nil { t.Fatalf("query reward failed: %v", err) } if row.ProductID != 102 { t.Fatalf("expected product_id=102, got=%d", row.ProductID) } if row.PriceSnapshotCents != 2300 { t.Fatalf("expected snapshot=2300, got=%d", row.PriceSnapshotCents) } if row.CostSnapshotCents != 1300 { t.Fatalf("expected cost snapshot=1300, got=%d", row.CostSnapshotCents) } } func TestCopyActivity_CopiesRewardSnapshotsAndMissingFields(t *testing.T) { svc, q, db := newRewardSnapshotTestService(t) ctx := context.Background() snapshotAt := time.Date(2026, 4, 21, 10, 0, 0, 0, time.UTC) if err := db.Exec("INSERT INTO activities (id, name, banner, activity_category_id, status, price_draw, is_boss, end_time) VALUES (118, '源活动', 'banner', 3, 1, 199, 0, CURRENT_TIMESTAMP)").Error; err != nil { t.Fatalf("insert activity failed: %v", err) } if err := db.Exec("INSERT INTO activity_issues (id, activity_id, issue_number, status, sort) VALUES (201, 118, '001', 1, 9)").Error; err != nil { t.Fatalf("insert issue failed: %v", err) } if err := db.Exec("INSERT INTO activity_reward_settings (issue_id, product_id, price_snapshot_cents, cost_snapshot_cents, price_snapshot_at, weight, quantity, original_qty, level, sort, is_boss, min_score, drop_quantity) VALUES (201, 101, 1800, 900, ?, 5, 10, 10, 1, 7, 0, 88, 3)", snapshotAt).Error; err != nil { t.Fatalf("insert reward failed: %v", err) } newActivityID, err := svc.CopyActivity(ctx, 118) if err != nil { t.Fatalf("CopyActivity failed: %v", err) } if newActivityID == 118 || newActivityID == 0 { t.Fatalf("expected new activity id, got=%d", newActivityID) } issues, err := q.ActivityIssues.WithContext(ctx).Where(q.ActivityIssues.ActivityID.Eq(newActivityID)).Find() if err != nil || len(issues) != 1 { t.Fatalf("query new issues failed: %v len=%d", err, len(issues)) } reward, err := q.ActivityRewardSettings.WithContext(ctx).Where(q.ActivityRewardSettings.IssueID.Eq(issues[0].ID)).First() if err != nil { t.Fatalf("query copied reward failed: %v", err) } if reward.PriceSnapshotCents != 1800 || reward.CostSnapshotCents != 900 { t.Fatalf("unexpected copied snapshots: price=%d cost=%d", reward.PriceSnapshotCents, reward.CostSnapshotCents) } if !reward.PriceSnapshotAt.Equal(snapshotAt) { t.Fatalf("expected copied snapshot time %v, got %v", snapshotAt, reward.PriceSnapshotAt) } if reward.MinScore != 88 || reward.DropQuantity != 3 { t.Fatalf("expected min_score/drop_quantity copied, got min_score=%d drop_quantity=%d", reward.MinScore, reward.DropQuantity) } } func TestCopyActivity_FillsMissingSnapshotData(t *testing.T) { svc, q, db := newRewardSnapshotTestService(t) ctx := context.Background() if err := db.Exec("INSERT INTO products (id, name, price, cost_price, stock, images_json) VALUES (101, 'A', 2600, 1700, 10, '[]')").Error; err != nil { t.Fatalf("insert product failed: %v", err) } if err := db.Exec("INSERT INTO activities (id, name, banner, activity_category_id, status, price_draw, is_boss, end_time) VALUES (118, '源活动', 'banner', 3, 1, 199, 0, CURRENT_TIMESTAMP)").Error; err != nil { t.Fatalf("insert activity failed: %v", err) } if err := db.Exec("INSERT INTO activity_issues (id, activity_id, issue_number, status, sort) VALUES (201, 118, '001', 1, 9)").Error; err != nil { t.Fatalf("insert issue failed: %v", err) } if err := db.Exec("INSERT INTO activity_reward_settings (issue_id, product_id, price_snapshot_cents, cost_snapshot_cents, weight, quantity, original_qty, level, sort, is_boss, min_score, drop_quantity) VALUES (201, 101, 0, 0, 5, 10, 10, 1, 7, 0, 0, 0)").Error; err != nil { t.Fatalf("insert legacy reward failed: %v", err) } newActivityID, err := svc.CopyActivity(ctx, 118) if err != nil { t.Fatalf("CopyActivity failed: %v", err) } issues, err := q.ActivityIssues.WithContext(ctx).Where(q.ActivityIssues.ActivityID.Eq(newActivityID)).Find() if err != nil || len(issues) != 1 { t.Fatalf("query new issues failed: %v len=%d", err, len(issues)) } reward, err := q.ActivityRewardSettings.WithContext(ctx).Where(q.ActivityRewardSettings.IssueID.Eq(issues[0].ID)).First() if err != nil { t.Fatalf("query copied reward failed: %v", err) } if reward.PriceSnapshotCents != 2600 || reward.CostSnapshotCents != 1700 { t.Fatalf("expected fallback snapshots from product, got price=%d cost=%d", reward.PriceSnapshotCents, reward.CostSnapshotCents) } if reward.PriceSnapshotAt.IsZero() { t.Fatalf("expected snapshot time to be backfilled") } if reward.DropQuantity != 1 { t.Fatalf("expected default drop quantity 1, got %d", reward.DropQuantity) } }