package promo import ( "encoding/json" "fmt" "math" "time" promomodel "github.com/perfect-panel/server/internal/model/promo" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) const ( ruleCacheKey = "promo:rules:enabled" subscribeCachePref = "promo:subscribe:" ) var ( minPromoRuleTime = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) maxPromoRuleTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC) ) func validateRuleInput(ruleType string, params map[string]interface{}, priority int64, startTime, endTime *int64) error { if priority < 0 { return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "priority must be greater than or equal to 0") } startAt, err := normalizeRuleTimestamp(startTime) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "invalid start_time") } endAt, err := normalizeRuleTimestamp(endTime) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "invalid end_time") } if startAt != nil && endAt != nil && !startAt.Before(*endAt) { return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "start_time must be less than end_time") } switch ruleType { case "new_user": windowHours, ok := numberParam(params, "window_hours") if !ok || windowHours <= 0 { return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "params.window_hours must be greater than 0") } case "inactive_user": inactiveMonths, ok := numberParam(params, "inactive_months") if !ok || inactiveMonths <= 0 { return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "params.inactive_months must be greater than 0") } case "campaign": if params == nil { return nil } default: return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "unsupported promo rule type") } return nil } func numberParam(params map[string]interface{}, key string) (int64, bool) { if params == nil { return 0, false } value, ok := params[key] if !ok { return 0, false } switch v := value.(type) { case float64: if math.Trunc(v) != v { return 0, false } return int64(v), true case int64: return v, true case int: return int64(v), true case json.Number: n, err := v.Int64() return n, err == nil default: return 0, false } } func paramsToString(params map[string]interface{}) (string, error) { if params == nil { params = map[string]interface{}{} } b, err := json.Marshal(params) if err != nil { return "", errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "invalid params") } return string(b), nil } func parseParams(data string) map[string]interface{} { if data == "" { return map[string]interface{}{} } var params map[string]interface{} if err := json.Unmarshal([]byte(data), ¶ms); err != nil { return map[string]interface{}{} } return params } func normalizeRuleTimestamp(ts *int64) (*time.Time, error) { if ts == nil || *ts == 0 { return nil, nil } value := *ts var t time.Time if value >= 1_000_000_000_000 || value <= -1_000_000_000_000 { t = time.UnixMilli(value) } else { t = time.Unix(value, 0) } if t.Before(minPromoRuleTime) || t.After(maxPromoRuleTime) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "timestamp out of range") } return &t, nil } func unixPtrToTimePtr(ts *int64) *time.Time { t, err := normalizeRuleTimestamp(ts) if err != nil { return nil } return t } func timePtrToUnixPtr(t *time.Time) *int64 { if t == nil { return nil } ts := t.Unix() return &ts } func convertRule(item *promomodel.Rule) types.PromoRule { if item == nil { return types.PromoRule{} } return types.PromoRule{ Id: item.Id, Name: item.Name, Type: item.Type, Params: parseParams(item.Params), Priority: item.Priority, Enabled: item.Enabled, StartTime: timePtrToUnixPtr(item.StartTime), EndTime: timePtrToUnixPtr(item.EndTime), CreatedAt: item.CreatedAt.Unix(), UpdatedAt: item.UpdatedAt.Unix(), } } func convertPrice(item *promomodel.SubscribePromo) types.PromoPrice { if item == nil { return types.PromoPrice{} } return types.PromoPrice{ Id: item.Id, SubscribeId: item.SubscribeId, PromoRuleId: item.PromoRuleId, Quantity: item.Quantity, PromoPrice: item.PromoPrice, CreatedAt: item.CreatedAt.Unix(), UpdatedAt: item.UpdatedAt.Unix(), } } func convertUsage(item *promomodel.Usage) types.PromoUsage { if item == nil { return types.PromoUsage{} } return types.PromoUsage{ Id: item.Id, UserId: item.UserId, PromoRuleId: item.PromoRuleId, SubscribeId: item.SubscribeId, OrderNo: item.OrderNo, PromoPrice: item.PromoPrice, CreatedAt: item.CreatedAt.Unix(), } } func subscribePromoCacheKey(subscribeId int64) string { return fmt.Sprintf("%s%d", subscribeCachePref, subscribeId) }