package validator import ( "errors" "regexp" "strings" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" ) type RewardValidator interface { ValidateCreateRewardRequest(req *contract.CreateRewardRequest) (error, string) ValidateUpdateRewardRequest(req *contract.UpdateRewardRequest) (error, string) ValidateListRewardsRequest(req *contract.ListRewardsRequest) (error, string) ValidateGetRewardRequest(req *contract.GetRewardRequest) (error, string) ValidateDeleteRewardRequest(req *contract.DeleteRewardRequest) (error, string) } type RewardValidatorImpl struct{} func NewRewardValidator() RewardValidator { return &RewardValidatorImpl{} } func (v *RewardValidatorImpl) ValidateCreateRewardRequest(req *contract.CreateRewardRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } // Validate name if strings.TrimSpace(req.Name) == "" { return errors.New("reward name is required"), constants.ValidationErrorCode } if len(req.Name) > 150 { return errors.New("reward name cannot exceed 150 characters"), constants.ValidationErrorCode } // Validate reward type if !v.isValidRewardType(req.RewardType) { return errors.New("invalid reward type. Valid types are: VOUCHER, PHYSICAL, DIGITAL, BALANCE"), constants.ValidationErrorCode } // Validate cost points if req.CostPoints <= 0 { return errors.New("cost points must be greater than 0"), constants.ValidationErrorCode } // Validate stock if req.Stock != nil && *req.Stock < 0 { return errors.New("stock cannot be negative"), constants.ValidationErrorCode } // Validate max per customer if req.MaxPerCustomer <= 0 { return errors.New("max per customer must be greater than 0"), constants.ValidationErrorCode } // Validate TNC if provided if req.Tnc != nil { if err := v.validateTermsAndConditions(req.Tnc); err != nil { return err, constants.ValidationErrorCode } } // Validate images if provided if req.Images != nil { if err := v.validateImages(*req.Images); err != nil { return err, constants.ValidationErrorCode } } return nil, "" } func (v *RewardValidatorImpl) ValidateUpdateRewardRequest(req *contract.UpdateRewardRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } // Validate ID if req.ID.String() == "" { return errors.New("reward ID is required"), constants.ValidationErrorCode } // Validate name if strings.TrimSpace(req.Name) == "" { return errors.New("reward name is required"), constants.ValidationErrorCode } if len(req.Name) > 150 { return errors.New("reward name cannot exceed 150 characters"), constants.ValidationErrorCode } // Validate reward type if !v.isValidRewardType(req.RewardType) { return errors.New("invalid reward type. Valid types are: VOUCHER, PHYSICAL, DIGITAL, BALANCE"), constants.ValidationErrorCode } // Validate cost points if req.CostPoints <= 0 { return errors.New("cost points must be greater than 0"), constants.ValidationErrorCode } // Validate stock if req.Stock != nil && *req.Stock < 0 { return errors.New("stock cannot be negative"), constants.ValidationErrorCode } // Validate max per customer if req.MaxPerCustomer <= 0 { return errors.New("max per customer must be greater than 0"), constants.ValidationErrorCode } // Validate TNC if provided if req.Tnc != nil { if err := v.validateTermsAndConditions(req.Tnc); err != nil { return err, constants.ValidationErrorCode } } // Validate images if provided if req.Images != nil { if err := v.validateImages(*req.Images); err != nil { return err, constants.ValidationErrorCode } } return nil, "" } func (v *RewardValidatorImpl) ValidateListRewardsRequest(req *contract.ListRewardsRequest) (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 reward type filter if provided if req.RewardType != "" && !v.isValidRewardType(req.RewardType) { return errors.New("invalid reward type filter. Valid types are: VOUCHER, PHYSICAL, DIGITAL, BALANCE"), constants.ValidationErrorCode } // Validate points range if provided if req.MinPoints != nil && req.MaxPoints != nil { if *req.MinPoints > *req.MaxPoints { return errors.New("min points cannot be greater than max points"), constants.ValidationErrorCode } if *req.MinPoints < 0 { return errors.New("min points cannot be negative"), constants.ValidationErrorCode } if *req.MaxPoints < 0 { return errors.New("max points cannot be negative"), constants.ValidationErrorCode } } return nil, "" } func (v *RewardValidatorImpl) ValidateGetRewardRequest(req *contract.GetRewardRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } if req.ID.String() == "" { return errors.New("reward ID is required"), constants.ValidationErrorCode } return nil, "" } func (v *RewardValidatorImpl) ValidateDeleteRewardRequest(req *contract.DeleteRewardRequest) (error, string) { if req == nil { return errors.New("request is required"), constants.ValidationErrorCode } if req.ID.String() == "" { return errors.New("reward ID is required"), constants.ValidationErrorCode } return nil, "" } func (v *RewardValidatorImpl) isValidRewardType(rewardType string) bool { validTypes := []string{"VOUCHER", "PHYSICAL", "DIGITAL", "BALANCE"} return contains(validTypes, rewardType) } func (v *RewardValidatorImpl) validateTermsAndConditions(tnc *contract.TermsAndConditionsStruct) error { if tnc == nil { return nil } // Validate expiry days if tnc.ExpiryDays < 0 { return errors.New("expiry days cannot be negative") } // Validate sections if len(tnc.Sections) == 0 { return errors.New("terms and conditions must have at least one section") } for i, section := range tnc.Sections { if strings.TrimSpace(section.Title) == "" { return errors.New("section title is required") } if len(section.Rules) == 0 { return errors.New("section must have at least one rule") } for j, rule := range section.Rules { if strings.TrimSpace(rule) == "" { return errors.New("rule cannot be empty") } if len(rule) > 500 { return errors.New("rule cannot exceed 500 characters") } // Validate rule content (basic validation) if len(rule) < 10 { return errors.New("rule must be at least 10 characters long") } _ = j // Avoid unused variable warning } _ = i // Avoid unused variable warning } return nil } func (v *RewardValidatorImpl) validateImages(images []string) error { if len(images) == 0 { return errors.New("at least one image is required") } for i, imgURL := range images { if strings.TrimSpace(imgURL) == "" { return errors.New("image URL is required") } // Validate URL format (basic validation) if !v.isValidURL(imgURL) { return errors.New("invalid image URL format") } _ = i // Avoid unused variable warning } return nil } func (v *RewardValidatorImpl) isValidURL(url string) bool { // Basic URL validation regex urlRegex := regexp.MustCompile(`^https?://[^\s/$.?#].[^\s]*$`) return urlRegex.MatchString(url) }