package validator import ( "errors" "strings" "time" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" ) type CampaignValidator interface { ValidateCreateCampaignRequest(req *contract.CreateCampaignRequest) (error, string) ValidateUpdateCampaignRequest(req *contract.UpdateCampaignRequest) (error, string) ValidateListCampaignsRequest(req *contract.ListCampaignsRequest) (error, string) ValidateGetCampaignRequest(req *contract.GetCampaignRequest) (error, string) ValidateDeleteCampaignRequest(req *contract.DeleteCampaignRequest) (error, string) ValidateCreateCampaignRuleRequest(req *contract.CreateCampaignRuleRequest) (error, string) ValidateUpdateCampaignRuleRequest(req *contract.UpdateCampaignRuleRequest) (error, string) ValidateListCampaignRulesRequest(req *contract.ListCampaignRulesRequest) (error, string) ValidateGetCampaignRuleRequest(req *contract.GetCampaignRuleRequest) (error, string) ValidateDeleteCampaignRuleRequest(req *contract.DeleteCampaignRuleRequest) (error, string) } type CampaignValidatorImpl struct{} func NewCampaignValidator() CampaignValidator { return &CampaignValidatorImpl{} } func (v *CampaignValidatorImpl) ValidateCreateCampaignRequest(req *contract.CreateCampaignRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } // Validate name if strings.TrimSpace(req.Name) == "" { return errors.New("campaign name is required"), constants.ValidationErrorCode } if len(req.Name) > 150 { return errors.New("campaign name cannot exceed 150 characters"), constants.ValidationErrorCode } // Validate campaign type if !v.isValidCampaignType(req.Type) { return errors.New("invalid campaign type. Valid types are: REWARD, POINTS, TOKENS, MIXED"), constants.ValidationErrorCode } // Validate date range if err := v.validateDateRange(req.StartDate, req.EndDate); err != nil { return err, constants.ValidationErrorCode } // Validate position if req.Position < 0 { return errors.New("position cannot be negative"), constants.ValidationErrorCode } // Validate rules if len(req.Rules) == 0 { return errors.New("at least one rule is required"), constants.ValidationErrorCode } for i, rule := range req.Rules { if err := v.validateCampaignRule(&rule, i+1); err != nil { return err, constants.ValidationErrorCode } } return nil, "" } func (v *CampaignValidatorImpl) ValidateUpdateCampaignRequest(req *contract.UpdateCampaignRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } // Validate ID if req.ID.String() == "" { return errors.New("campaign ID is required"), constants.ValidationErrorCode } // Validate name if strings.TrimSpace(req.Name) == "" { return errors.New("campaign name is required"), constants.ValidationErrorCode } if len(req.Name) > 150 { return errors.New("campaign name cannot exceed 150 characters"), constants.ValidationErrorCode } // Validate campaign type if !v.isValidCampaignType(req.Type) { return errors.New("invalid campaign type. Valid types are: REWARD, POINTS, TOKENS, MIXED"), constants.ValidationErrorCode } // Validate date range if err := v.validateDateRange(req.StartDate, req.EndDate); err != nil { return err, constants.ValidationErrorCode } // Validate position if req.Position < 0 { return errors.New("position cannot be negative"), constants.ValidationErrorCode } // Validate rules if len(req.Rules) == 0 { return errors.New("at least one rule is required"), constants.ValidationErrorCode } for i, rule := range req.Rules { if err := v.validateCampaignRule(&rule, i+1); err != nil { return err, constants.ValidationErrorCode } } return nil, "" } func (v *CampaignValidatorImpl) ValidateListCampaignsRequest(req *contract.ListCampaignsRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } // Validate pagination if req.Page < 1 { return errors.New("page must be greater than 0"), constants.ValidationErrorCode } if req.Limit < 1 { return errors.New("limit must be greater than 0"), constants.ValidationErrorCode } if req.Limit > 100 { return errors.New("limit cannot exceed 100"), constants.ValidationErrorCode } // Validate campaign type filter if provided if req.Type != "" && !v.isValidCampaignType(req.Type) { return errors.New("invalid campaign type filter. Valid types are: REWARD, POINTS, TOKENS, MIXED"), constants.ValidationErrorCode } return nil, "" } func (v *CampaignValidatorImpl) ValidateGetCampaignRequest(req *contract.GetCampaignRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } if req.ID.String() == "" { return errors.New("campaign ID is required"), constants.ValidationErrorCode } return nil, "" } func (v *CampaignValidatorImpl) ValidateDeleteCampaignRequest(req *contract.DeleteCampaignRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } if req.ID.String() == "" { return errors.New("campaign ID is required"), constants.ValidationErrorCode } return nil, "" } func (v *CampaignValidatorImpl) ValidateCreateCampaignRuleRequest(req *contract.CreateCampaignRuleRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } // Validate campaign ID if req.CampaignID.String() == "" { return errors.New("campaign ID is required"), constants.ValidationErrorCode } // Validate rule if err := v.validateCampaignRule(&contract.CampaignRuleStruct{ RuleType: req.RuleType, ConditionValue: req.ConditionValue, RewardType: req.RewardType, RewardValue: req.RewardValue, RewardSubtype: req.RewardSubtype, RewardRefID: req.RewardRefID, Metadata: req.Metadata, }, 1); err != nil { return err, constants.ValidationErrorCode } return nil, "" } func (v *CampaignValidatorImpl) ValidateUpdateCampaignRuleRequest(req *contract.UpdateCampaignRuleRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } // Validate ID if req.ID.String() == "" { return errors.New("campaign rule ID is required"), constants.ValidationErrorCode } // Validate campaign ID if req.CampaignID.String() == "" { return errors.New("campaign ID is required"), constants.ValidationErrorCode } // Validate rule if err := v.validateCampaignRule(&contract.CampaignRuleStruct{ RuleType: req.RuleType, ConditionValue: req.ConditionValue, RewardType: req.RewardType, RewardValue: req.RewardValue, RewardSubtype: req.RewardSubtype, RewardRefID: req.RewardRefID, Metadata: req.Metadata, }, 1); err != nil { return err, constants.ValidationErrorCode } return nil, "" } func (v *CampaignValidatorImpl) ValidateListCampaignRulesRequest(req *contract.ListCampaignRulesRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } // Validate pagination if req.Page < 1 { return errors.New("page must be greater than 0"), constants.ValidationErrorCode } if req.Limit < 1 { return errors.New("limit must be greater than 0"), constants.ValidationErrorCode } if req.Limit > 100 { return errors.New("limit cannot exceed 100"), constants.ValidationErrorCode } // Validate rule type filter if provided if req.RuleType != "" && !v.isValidRuleType(req.RuleType) { return errors.New("invalid rule type filter. Valid types are: TIER, SPEND, PRODUCT, CATEGORY, DAY, LOCATION"), constants.ValidationErrorCode } // Validate reward type filter if provided if req.RewardType != "" && !v.isValidRewardType(req.RewardType) { return errors.New("invalid reward type filter. Valid types are: POINTS, TOKENS, REWARD"), constants.ValidationErrorCode } return nil, "" } func (v *CampaignValidatorImpl) ValidateGetCampaignRuleRequest(req *contract.GetCampaignRuleRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } if req.ID.String() == "" { return errors.New("campaign rule ID is required"), constants.ValidationErrorCode } return nil, "" } func (v *CampaignValidatorImpl) ValidateDeleteCampaignRuleRequest(req *contract.DeleteCampaignRuleRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } if req.ID.String() == "" { return errors.New("campaign rule ID is required"), constants.ValidationErrorCode } return nil, "" } func (v *CampaignValidatorImpl) isValidCampaignType(campaignType string) bool { validTypes := []string{"REWARD", "POINTS", "TOKENS", "MIXED"} return contains(validTypes, campaignType) } func (v *CampaignValidatorImpl) isValidRuleType(ruleType string) bool { validTypes := []string{"TIER", "SPEND", "PRODUCT", "CATEGORY", "DAY", "LOCATION"} return contains(validTypes, ruleType) } func (v *CampaignValidatorImpl) isValidRewardType(rewardType string) bool { validTypes := []string{"POINTS", "TOKENS", "REWARD"} return contains(validTypes, rewardType) } func (v *CampaignValidatorImpl) validateDateRange(startDate, endDate time.Time) error { if startDate.IsZero() { return errors.New("start date is required") } if endDate.IsZero() { return errors.New("end date is required") } if startDate.After(endDate) { return errors.New("start date cannot be after end date") } return nil } func (v *CampaignValidatorImpl) validateCampaignRule(rule *contract.CampaignRuleStruct, ruleNumber int) error { if rule == nil { return errors.New("rule is required") } // Validate rule type if !v.isValidRuleType(rule.RuleType) { return errors.New("invalid rule type in rule " + string(rune(ruleNumber)) + ". Valid types are: TIER, SPEND, PRODUCT, CATEGORY, DAY, LOCATION") } // Validate reward type if !v.isValidRewardType(rule.RewardType) { return errors.New("invalid reward type in rule " + string(rune(ruleNumber)) + ". Valid types are: POINTS, TOKENS, REWARD") } // Validate reward value based on reward type if rule.RewardType == "POINTS" || rule.RewardType == "TOKENS" { if rule.RewardValue == nil || *rule.RewardValue <= 0 { return errors.New("reward value must be positive for " + rule.RewardType + " type in rule " + string(rune(ruleNumber))) } } // Validate reward reference ID for REWARD type if rule.RewardType == "REWARD" { if rule.RewardRefID == nil { return errors.New("reward reference ID is required for REWARD type in rule " + string(rune(ruleNumber))) } } return nil }