package promo import ( "context" "encoding/json" "fmt" "time" promoModel "github.com/perfect-panel/server/internal/model/promo" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) const ( enabledRulesCacheKey = "promo:rules:enabled" subscribePromoKeyFmt = "promo:subscribe:%d" ) type ruleParams struct { WindowHours int `json:"window_hours"` InactiveMonths int `json:"inactive_months"` } func validateRulePayload(ruleType string, params json.RawMessage, priority, startTime, endTime int64) error { if priority < 0 { return xerr.NewErrCodeMsg(xerr.InvalidParams, "priority must be greater than or equal to 0") } if startTime >= endTime { return xerr.NewErrCodeMsg(xerr.InvalidParams, "start_time must be less than end_time") } var p ruleParams if len(params) == 0 || !json.Valid(params) { return xerr.NewErrCodeMsg(xerr.InvalidParams, "params must be valid json") } if err := json.Unmarshal(params, &p); err != nil { return xerr.NewErrCodeMsg(xerr.InvalidParams, "params must be valid json object") } switch ruleType { case promoModel.RuleTypeNewUser: if p.WindowHours <= 0 { return xerr.NewErrCodeMsg(xerr.InvalidParams, "params.window_hours must be greater than 0") } case promoModel.RuleTypeInactiveUser: if p.InactiveMonths <= 0 { return xerr.NewErrCodeMsg(xerr.InvalidParams, "params.inactive_months must be greater than 0") } case promoModel.RuleTypeCampaign: return nil default: return xerr.NewErrCodeMsg(xerr.InvalidParams, "type must be new_user, inactive_user or campaign") } return nil } func toRuleResponse(data *promoModel.Rule) types.PromoRule { resp := types.PromoRule{ Id: data.Id, Name: data.Name, Type: data.Type, Params: json.RawMessage(data.Params), Priority: data.Priority, Enabled: data.Enabled, CreatedAt: data.CreatedAt.UnixMilli(), UpdatedAt: data.UpdatedAt.UnixMilli(), } if data.StartTime != nil { resp.StartTime = data.StartTime.UnixMilli() } if data.EndTime != nil { resp.EndTime = data.EndTime.UnixMilli() } return resp } func toPriceResponse(data *promoModel.SubscribePromo, unitPrice int64) types.PromoPrice { return types.PromoPrice{ Id: data.Id, SubscribeId: data.SubscribeId, PromoRuleId: data.PromoRuleId, PromoPrice: data.PromoPrice, UnitPrice: unitPrice, CreatedAt: data.CreatedAt.UnixMilli(), UpdatedAt: data.UpdatedAt.UnixMilli(), } } func toUsageResponse(data *promoModel.Usage) types.PromoUsage { return types.PromoUsage{ Id: data.Id, UserId: data.UserId, PromoRuleId: data.PromoRuleId, SubscribeId: data.SubscribeId, OrderNo: data.OrderNo, PromoPrice: data.PromoPrice, CreatedAt: data.CreatedAt.UnixMilli(), } } func ruleTimes(startTime, endTime int64) (*time.Time, *time.Time) { start := time.UnixMilli(startTime) end := time.UnixMilli(endTime) return &start, &end } func clearRuleCache(ctx context.Context, svcCtx *svc.ServiceContext) { if svcCtx != nil && svcCtx.Redis != nil { _ = svcCtx.Redis.Del(ctx, enabledRulesCacheKey).Err() } } func clearSubscribePromoCache(ctx context.Context, svcCtx *svc.ServiceContext, ids ...int64) { if svcCtx == nil || svcCtx.Redis == nil || len(ids) == 0 { return } keys := make([]string, 0, len(ids)) for _, id := range ids { keys = append(keys, fmt.Sprintf(subscribePromoKeyFmt, id)) } _ = svcCtx.Redis.Del(ctx, keys...).Err() } func wrapQueryError(msg string, err error) error { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "%s: %v", msg, err.Error()) }