新功能(#75): 实现促销资格判定核心逻辑
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -0,0 +1,334 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
stderrors "errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PromoRuleTypeNewUser = "new_user"
|
||||||
|
PromoRuleTypeInactiveUser = "inactive_user"
|
||||||
|
PromoRuleTypeCampaign = "campaign"
|
||||||
|
|
||||||
|
promoRulesEnabledCacheKey = "promo:rules:enabled"
|
||||||
|
promoSubscribeCachePrefix = "promo:subscribe:"
|
||||||
|
promoCacheTTL = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type PromoResult struct {
|
||||||
|
Eligible bool
|
||||||
|
RuleID int64
|
||||||
|
RuleName string
|
||||||
|
RuleType string
|
||||||
|
PromoPrice int64
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type promoRule struct {
|
||||||
|
Id int64 `gorm:"column:id" json:"id"`
|
||||||
|
Name string `gorm:"column:name" json:"name"`
|
||||||
|
Type string `gorm:"column:type" json:"type"`
|
||||||
|
Params string `gorm:"column:params" json:"params"`
|
||||||
|
Priority int64 `gorm:"column:priority" json:"priority"`
|
||||||
|
Enabled bool `gorm:"column:enabled" json:"enabled"`
|
||||||
|
StartTime *time.Time `gorm:"column:start_time" json:"start_time,omitempty"`
|
||||||
|
EndTime *time.Time `gorm:"column:end_time" json:"end_time,omitempty"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"deleted_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (promoRule) TableName() string {
|
||||||
|
return "promo_rule"
|
||||||
|
}
|
||||||
|
|
||||||
|
type subscribePromo struct {
|
||||||
|
Id int64 `gorm:"column:id" json:"id"`
|
||||||
|
SubscribeId int64 `gorm:"column:subscribe_id" json:"subscribe_id"`
|
||||||
|
PromoRuleId int64 `gorm:"column:promo_rule_id" json:"promo_rule_id"`
|
||||||
|
PromoPrice int64 `gorm:"column:promo_price" json:"promo_price"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (subscribePromo) TableName() string {
|
||||||
|
return "subscribe_promo"
|
||||||
|
}
|
||||||
|
|
||||||
|
type promoRuleParams struct {
|
||||||
|
WindowHours int64 `json:"window_hours"`
|
||||||
|
InactiveMonths int `json:"inactive_months"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type promoEligibilitySource interface {
|
||||||
|
UserCreatedAt(ctx context.Context, userID int64) (time.Time, error)
|
||||||
|
LastSubscribeExpireAt(ctx context.Context, userID int64) (time.Time, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type gormPromoEligibilitySource struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func EvaluatePromo(ctx context.Context, svcCtx *svc.ServiceContext, userID int64, subscribeID int64) (*PromoResult, error) {
|
||||||
|
if svcCtx == nil || svcCtx.DB == nil {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "service context is empty")
|
||||||
|
}
|
||||||
|
if userID <= 0 || subscribeID <= 0 {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "user id or subscribe id is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, err := loadEnabledPromoRules(ctx, svcCtx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return &PromoResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
prices, err := loadSubscribePromos(ctx, svcCtx, subscribeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(prices) == 0 {
|
||||||
|
return &PromoResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return evaluatePromoRulesAt(ctx, rules, prices, &gormPromoEligibilitySource{db: svcCtx.DB}, userID, time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluatePromoRulesAt(
|
||||||
|
ctx context.Context,
|
||||||
|
rules []promoRule,
|
||||||
|
prices []subscribePromo,
|
||||||
|
source promoEligibilitySource,
|
||||||
|
userID int64,
|
||||||
|
now time.Time,
|
||||||
|
) (*PromoResult, error) {
|
||||||
|
priceByRuleID := make(map[int64]int64, len(prices))
|
||||||
|
for _, item := range prices {
|
||||||
|
if item.PromoRuleId <= 0 || item.PromoPrice <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
priceByRuleID[item.PromoRuleId] = item.PromoPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
var userCreatedAt time.Time
|
||||||
|
var userCreatedAtLoaded bool
|
||||||
|
var lastExpireAt time.Time
|
||||||
|
var lastExpireAtLoaded bool
|
||||||
|
|
||||||
|
for _, rule := range rules {
|
||||||
|
promoPrice, ok := priceByRuleID[rule.Id]
|
||||||
|
if !ok || !rule.isAvailableAt(now) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
params, err := rule.params()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var eligible bool
|
||||||
|
var expiresAt time.Time
|
||||||
|
switch rule.Type {
|
||||||
|
case PromoRuleTypeNewUser:
|
||||||
|
if !userCreatedAtLoaded {
|
||||||
|
userCreatedAt, err = source.UserCreatedAt(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userCreatedAtLoaded = true
|
||||||
|
}
|
||||||
|
eligible, expiresAt = evaluatePromoNewUser(userCreatedAt, params, now)
|
||||||
|
case PromoRuleTypeInactiveUser:
|
||||||
|
if !lastExpireAtLoaded {
|
||||||
|
lastExpireAt, err = source.LastSubscribeExpireAt(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lastExpireAtLoaded = true
|
||||||
|
}
|
||||||
|
eligible, expiresAt = evaluatePromoInactiveUser(lastExpireAt, params, rule, now)
|
||||||
|
case PromoRuleTypeCampaign:
|
||||||
|
eligible, expiresAt = evaluatePromoCampaign(rule)
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !eligible {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return &PromoResult{
|
||||||
|
Eligible: true,
|
||||||
|
RuleID: rule.Id,
|
||||||
|
RuleName: rule.Name,
|
||||||
|
RuleType: rule.Type,
|
||||||
|
PromoPrice: promoPrice,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PromoResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluatePromoNewUser(userCreatedAt time.Time, params promoRuleParams, now time.Time) (bool, time.Time) {
|
||||||
|
if userCreatedAt.IsZero() || params.WindowHours <= 0 {
|
||||||
|
return false, time.Time{}
|
||||||
|
}
|
||||||
|
expiresAt := userCreatedAt.Add(time.Duration(params.WindowHours) * time.Hour)
|
||||||
|
return now.Before(expiresAt), expiresAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluatePromoInactiveUser(lastExpireAt time.Time, params promoRuleParams, rule promoRule, now time.Time) (bool, time.Time) {
|
||||||
|
if params.InactiveMonths <= 0 {
|
||||||
|
return false, time.Time{}
|
||||||
|
}
|
||||||
|
if lastExpireAt.Equal(time.UnixMilli(0)) || lastExpireAt.After(now) {
|
||||||
|
return false, time.Time{}
|
||||||
|
}
|
||||||
|
if lastExpireAt.IsZero() {
|
||||||
|
return true, rule.expiresAt()
|
||||||
|
}
|
||||||
|
|
||||||
|
threshold := now.AddDate(0, -params.InactiveMonths, 0)
|
||||||
|
return !lastExpireAt.After(threshold), rule.expiresAt()
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluatePromoCampaign(rule promoRule) (bool, time.Time) {
|
||||||
|
return true, rule.expiresAt()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r promoRule) isAvailableAt(now time.Time) bool {
|
||||||
|
if r.Id <= 0 || !r.Enabled || r.DeletedAt.Valid {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if r.StartTime != nil && now.Before(*r.StartTime) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if r.EndTime != nil && now.After(*r.EndTime) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r promoRule) expiresAt() time.Time {
|
||||||
|
if r.EndTime == nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return *r.EndTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r promoRule) params() (promoRuleParams, error) {
|
||||||
|
if r.Params == "" {
|
||||||
|
return promoRuleParams{}, nil
|
||||||
|
}
|
||||||
|
var params promoRuleParams
|
||||||
|
if err := json.Unmarshal([]byte(r.Params), ¶ms); err != nil {
|
||||||
|
return promoRuleParams{}, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "parse promo rule params failed")
|
||||||
|
}
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *gormPromoEligibilitySource) UserCreatedAt(ctx context.Context, userID int64) (time.Time, error) {
|
||||||
|
var item user.User
|
||||||
|
err := s.db.WithContext(ctx).
|
||||||
|
Model(&user.User{}).
|
||||||
|
Where("id = ?", userID).
|
||||||
|
Take(&item).Error
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo user failed")
|
||||||
|
}
|
||||||
|
return item.CreatedAt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *gormPromoEligibilitySource) LastSubscribeExpireAt(ctx context.Context, userID int64) (time.Time, error) {
|
||||||
|
var item user.Subscribe
|
||||||
|
err := s.db.WithContext(ctx).
|
||||||
|
Model(&user.Subscribe{}).
|
||||||
|
Where("user_id = ?", userID).
|
||||||
|
Where("expire_time != ?", time.UnixMilli(0)).
|
||||||
|
Order("expire_time DESC").
|
||||||
|
Limit(1).
|
||||||
|
Take(&item).Error
|
||||||
|
if err != nil {
|
||||||
|
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return time.Time{}, nil
|
||||||
|
}
|
||||||
|
return time.Time{}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo user last subscription failed")
|
||||||
|
}
|
||||||
|
return item.ExpireTime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadEnabledPromoRules(ctx context.Context, svcCtx *svc.ServiceContext) ([]promoRule, error) {
|
||||||
|
if cached, ok := getPromoCache[[]promoRule](ctx, svcCtx, promoRulesEnabledCacheKey); ok {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules []promoRule
|
||||||
|
if err := svcCtx.DB.WithContext(ctx).
|
||||||
|
Model(&promoRule{}).
|
||||||
|
Where("enabled = ?", true).
|
||||||
|
Order("priority DESC").
|
||||||
|
Order("id ASC").
|
||||||
|
Find(&rules).Error; err != nil {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query enabled promo rules failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
setPromoCache(ctx, svcCtx, promoRulesEnabledCacheKey, rules)
|
||||||
|
return rules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSubscribePromos(ctx context.Context, svcCtx *svc.ServiceContext, subscribeID int64) ([]subscribePromo, error) {
|
||||||
|
cacheKey := fmt.Sprintf("%s%d", promoSubscribeCachePrefix, subscribeID)
|
||||||
|
if cached, ok := getPromoCache[[]subscribePromo](ctx, svcCtx, cacheKey); ok {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var promos []subscribePromo
|
||||||
|
if err := svcCtx.DB.WithContext(ctx).
|
||||||
|
Model(&subscribePromo{}).
|
||||||
|
Where("subscribe_id = ?", subscribeID).
|
||||||
|
Find(&promos).Error; err != nil {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query subscribe promos failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
setPromoCache(ctx, svcCtx, cacheKey, promos)
|
||||||
|
return promos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPromoCache[T any](ctx context.Context, svcCtx *svc.ServiceContext, key string) (T, bool) {
|
||||||
|
var zero T
|
||||||
|
if svcCtx == nil || svcCtx.Redis == nil {
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := svcCtx.Redis.Get(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var data T
|
||||||
|
if err = json.Unmarshal([]byte(value), &data); err != nil {
|
||||||
|
_ = svcCtx.Redis.Del(ctx, key).Err()
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
return data, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPromoCache(ctx context.Context, svcCtx *svc.ServiceContext, key string, value any) {
|
||||||
|
if svcCtx == nil || svcCtx.Redis == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = svcCtx.Redis.Set(ctx, key, string(payload), promoCacheTTL).Err()
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user