package subscribe import ( "context" "strings" "testing" "time" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/types" "gorm.io/driver/mysql" "gorm.io/gorm" ) func TestPromoEligibilityEvaluatorMatch(t *testing.T) { now := time.Unix(1710000000, 0) campaignEnd := now.Add(2 * time.Hour) campaign := subscribePromoCandidate{ RuleName: "限时活动", RuleType: promoRuleTypeCampaign, PromoPrice: 99, EndTime: &campaignEnd, } ok, expiresAt, err := (&promoEligibilityEvaluator{}).match(campaign, now) if err != nil { t.Fatalf("campaign match error: %v", err) } if !ok { t.Fatal("campaign promo should match without login") } if got, want := unixSeconds(expiresAt), campaignEnd.Unix(); got != want { t.Fatalf("campaign expires_at = %d, want %d", got, want) } newUser := subscribePromoCandidate{ RuleName: "新客7天优惠", RuleType: promoRuleTypeNewUser, PromoPrice: 279, Params: `{"window_hours":168}`, } ok, _, err = (&promoEligibilityEvaluator{}).match(newUser, now) if err != nil { t.Fatalf("anonymous new_user match error: %v", err) } if ok { t.Fatal("new_user promo should not match without login") } userInfo := &user.User{Id: 1, CreatedAt: now.Add(-24 * time.Hour)} ok, expiresAt, err = (&promoEligibilityEvaluator{userInfo: userInfo}).match(newUser, now) if err != nil { t.Fatalf("logged-in new_user match error: %v", err) } if !ok { t.Fatal("new_user promo should match inside window") } if got, want := unixSeconds(expiresAt), userInfo.CreatedAt.Add(168*time.Hour).Unix(); got != want { t.Fatalf("new_user expires_at = %d, want %d", got, want) } } func TestSubscribePromoCandidateActiveWindow(t *testing.T) { now := time.Unix(1710000000, 0) start := now.Add(-time.Hour) end := now.Add(time.Hour) if !(subscribePromoCandidate{PromoPrice: 1, StartTime: &start, EndTime: &end}).isActive(now) { t.Fatal("candidate inside active window should be active") } if (subscribePromoCandidate{PromoPrice: 0, StartTime: &start, EndTime: &end}).isActive(now) { t.Fatal("candidate with zero promo price should not be active") } if (subscribePromoCandidate{PromoPrice: 1, StartTime: &end}).isActive(now) { t.Fatal("candidate before start time should not be active") } if (subscribePromoCandidate{PromoPrice: 1, EndTime: &start}).isActive(now) { t.Fatal("candidate after end time should not be active") } } func TestLastSubscribeExpireAtPrioritizesPermanentSubscription(t *testing.T) { db, err := gorm.Open(mysql.New(mysql.Config{ DSN: "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local", SkipInitializeWithVersion: true, }), &gorm.Config{DryRun: true, DisableAutomaticPing: true}) if err != nil { t.Fatalf("open dry-run db: %v", err) } evaluator := &promoEligibilityEvaluator{ db: db, userInfo: &user.User{Id: 7}, } var item user.Subscribe tx := evaluator.lastSubscribeExpireQuery().Limit(1).Take(&item) sql := tx.Statement.SQL.String() if !strings.Contains(sql, "CASE WHEN expire_time = ? THEN 0 ELSE 1 END") { t.Fatalf("SQL missing permanent subscription priority order: %s", sql) } if strings.Contains(sql, "expire_time !=") { t.Fatalf("SQL should not filter out permanent subscriptions: %s", sql) } if len(tx.Statement.Vars) < 2 { t.Fatalf("SQL vars length = %d, want at least 2; vars=%v", len(tx.Statement.Vars), tx.Statement.Vars) } if got, want := tx.Statement.Vars[1], time.UnixMilli(0); got != want { t.Fatalf("permanent subscription order var = %v, want %v; vars=%v", got, want, tx.Statement.Vars) } } func TestQuerySubscribePromoCandidatesIncludesQuantity(t *testing.T) { db, err := gorm.Open(mysql.New(mysql.Config{ DSN: "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local", SkipInitializeWithVersion: true, }), &gorm.Config{DryRun: true, DisableAutomaticPing: true}) if err != nil { t.Fatalf("open dry-run db: %v", err) } var candidates []subscribePromoCandidate tx := subscribePromoCandidatesQuery(context.Background(), db, []int64{11, 12}, true).Scan(&candidates) stmt := tx.Statement sql := stmt.SQL.String() if !strings.Contains(sql, "sp.subscribe_id, sp.quantity, sp.promo_price") { t.Fatalf("SQL missing quantity select: %s", sql) } if !strings.Contains(sql, "ORDER BY sp.subscribe_id ASC,sp.quantity ASC,pr.priority DESC,pr.id ASC") { t.Fatalf("SQL missing quantity order: %s", sql) } } func TestApplySubscribeDiscountPromosMatchesQuantity(t *testing.T) { subscribe := types.Subscribe{Discount: []types.SubscribeDiscount{ {Quantity: 1}, {Quantity: 12}, }} promos := map[int64]*types.SubscribePromo{ 3: {RuleName: "季度优惠", PromoPrice: 2900}, 12: {RuleName: "年度优惠", PromoPrice: 9900}, } applySubscribeDiscountPromos(&subscribe, promos) if subscribe.Discount[0].Promo != nil { t.Fatalf("quantity 1 promo should be nil, got %+v", subscribe.Discount[0].Promo) } if subscribe.Discount[1].Promo == nil { t.Fatal("quantity 12 promo should match") } if got, want := subscribe.Discount[1].Promo.RuleName, "年度优惠"; got != want { t.Fatalf("promo rule name = %q, want %q", got, want) } subscribe = types.Subscribe{Discount: []types.SubscribeDiscount{{Quantity: 6}}} applySubscribeDiscountPromos(&subscribe, promos) if subscribe.Discount[0].Promo != nil { t.Fatalf("promo should be nil when quantity does not match, got %+v", subscribe.Discount[0].Promo) } }