package common import ( "context" "strings" "testing" "time" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" ) type fakePromoEligibilitySource struct { userCreatedAt time.Time lastExpireAt time.Time } func (s fakePromoEligibilitySource) UserCreatedAt(context.Context, int64) (time.Time, error) { return s.userCreatedAt, nil } func (s fakePromoEligibilitySource) LastSubscribeExpireAt(context.Context, int64) (time.Time, error) { return s.lastExpireAt, nil } func TestEvaluatePromoRulesAtPriorityFirstMatch(t *testing.T) { now := time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC) campaignEnd := now.Add(24 * time.Hour) rules := []promoRule{ { Id: 2, Name: "campaign", Type: PromoRuleTypeCampaign, Priority: 20, Enabled: true, EndTime: &campaignEnd, }, { Id: 1, Name: "new user", Type: PromoRuleTypeNewUser, Params: `{"window_hours":168}`, Priority: 10, Enabled: true, }, } prices := []subscribePromo{ {SubscribeId: 100, PromoRuleId: 1, PromoPrice: 599}, {SubscribeId: 100, PromoRuleId: 2, PromoPrice: 499}, } got, err := evaluatePromoRulesAt(context.Background(), rules, prices, fakePromoEligibilitySource{ userCreatedAt: now.Add(-time.Hour), }, 10, now) if err != nil { t.Fatalf("evaluatePromoRulesAt error: %v", err) } if !got.Eligible || got.RuleID != 2 || got.PromoPrice != 499 || got.RuleType != PromoRuleTypeCampaign { t.Fatalf("unexpected promo result: %+v", got) } if !got.ExpiresAt.Equal(campaignEnd) { t.Fatalf("expires_at = %v, want %v", got.ExpiresAt, campaignEnd) } } func TestEvaluatePromoRulesAtSkipsUnavailableRules(t *testing.T) { now := time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC) futureStart := now.Add(time.Hour) expiredEnd := now.Add(-time.Hour) rules := []promoRule{ {Id: 1, Type: PromoRuleTypeCampaign, Enabled: true, StartTime: &futureStart}, {Id: 2, Type: PromoRuleTypeCampaign, Enabled: true, EndTime: &expiredEnd}, {Id: 3, Type: PromoRuleTypeCampaign, Enabled: false}, {Id: 4, Type: "unknown", Enabled: true}, {Id: 5, Type: PromoRuleTypeCampaign, Enabled: true}, } prices := []subscribePromo{ {SubscribeId: 100, PromoRuleId: 1, PromoPrice: 100}, {SubscribeId: 100, PromoRuleId: 2, PromoPrice: 100}, {SubscribeId: 100, PromoRuleId: 3, PromoPrice: 100}, {SubscribeId: 100, PromoRuleId: 4, PromoPrice: 100}, {SubscribeId: 100, PromoRuleId: 5, PromoPrice: 88}, } got, err := evaluatePromoRulesAt(context.Background(), rules, prices, fakePromoEligibilitySource{}, 10, now) if err != nil { t.Fatalf("evaluatePromoRulesAt error: %v", err) } if !got.Eligible || got.RuleID != 5 || got.PromoPrice != 88 { t.Fatalf("unexpected promo result: %+v", got) } } func TestEvaluatePromoNewUser(t *testing.T) { now := time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC) createdAt := now.Add(-23 * time.Hour) eligible, expiresAt := evaluatePromoNewUser(createdAt, promoRuleParams{WindowHours: 24}, now) if !eligible { t.Fatal("new user should be eligible inside configured window") } if !expiresAt.Equal(createdAt.Add(24 * time.Hour)) { t.Fatalf("expires_at = %v, want %v", expiresAt, createdAt.Add(24*time.Hour)) } eligible, _ = evaluatePromoNewUser(createdAt, promoRuleParams{WindowHours: 12}, now) if eligible { t.Fatal("new user should not be eligible after configured window") } eligible, _ = evaluatePromoNewUser(createdAt, promoRuleParams{}, now) if eligible { t.Fatal("new user should not be eligible without positive window_hours") } } func TestEvaluatePromoInactiveUser(t *testing.T) { now := time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC) endTime := now.Add(48 * time.Hour) rule := promoRule{EndTime: &endTime} eligible, expiresAt := evaluatePromoInactiveUser(time.Time{}, promoRuleParams{InactiveMonths: 3}, rule, now) if !eligible { t.Fatal("never purchased user should be eligible for inactive promo") } if !expiresAt.Equal(endTime) { t.Fatalf("expires_at = %v, want %v", expiresAt, endTime) } eligible, _ = evaluatePromoInactiveUser(now.AddDate(0, -4, 0), promoRuleParams{InactiveMonths: 3}, rule, now) if !eligible { t.Fatal("expired before inactive threshold should be eligible") } eligible, _ = evaluatePromoInactiveUser(now.AddDate(0, -1, 0), promoRuleParams{InactiveMonths: 3}, rule, now) if eligible { t.Fatal("recently expired subscription should not be eligible") } eligible, _ = evaluatePromoInactiveUser(time.UnixMilli(0), promoRuleParams{InactiveMonths: 3}, rule, now) if eligible { t.Fatal("unlimited active subscription should not be eligible") } eligible, _ = evaluatePromoInactiveUser(now.Add(time.Hour), promoRuleParams{InactiveMonths: 3}, rule, now) if eligible { t.Fatal("currently active subscription should not be eligible") } eligible, _ = evaluatePromoInactiveUser(now.AddDate(0, -4, 0), promoRuleParams{}, rule, now) if eligible { t.Fatal("inactive promo should require positive inactive_months") } } func TestLastSubscribeExpireAtIncludesUnlimitedSubscription(t *testing.T) { db, err := gorm.Open(mysql.New(mysql.Config{ DSN: "gorm:password@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local", SkipInitializeWithVersion: true, }), &gorm.Config{ DryRun: true, DisableAutomaticPing: true, Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Fatalf("open gorm db: %v", err) } stmt := lastSubscribeExpireQuery(db, 10).Take(nil).Statement sql := strings.ToLower(stmt.SQL.String()) if strings.Contains(sql, "expire_time <>") || strings.Contains(sql, "expire_time !=") { t.Fatalf("last subscribe query should include unlimited subscription, sql: %s", stmt.SQL.String()) } if !strings.Contains(sql, "case when expire_time = ? then 0 else 1 end asc") { t.Fatalf("last subscribe query should prioritize unlimited subscription, sql: %s", stmt.SQL.String()) } }