Add Campaign
This commit is contained in:
parent
201e24041b
commit
155016dec8
@ -96,6 +96,10 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
validators.orderIngredientTransactionValidator,
|
||||
services.gamificationService,
|
||||
validators.gamificationValidator,
|
||||
services.rewardService,
|
||||
validators.rewardValidator,
|
||||
services.campaignService,
|
||||
validators.campaignValidator,
|
||||
)
|
||||
|
||||
return nil
|
||||
@ -176,6 +180,8 @@ type repositories struct {
|
||||
gamePrizeRepo *repository.GamePrizeRepository
|
||||
gamePlayRepo *repository.GamePlayRepository
|
||||
omsetTrackerRepo *repository.OmsetTrackerRepository
|
||||
rewardRepo repository.RewardRepository
|
||||
campaignRepo repository.CampaignRepository
|
||||
txManager *repository.TxManager
|
||||
}
|
||||
|
||||
@ -216,6 +222,8 @@ func (a *App) initRepositories() *repositories {
|
||||
gamePrizeRepo: repository.NewGamePrizeRepository(a.db),
|
||||
gamePlayRepo: repository.NewGamePlayRepository(a.db),
|
||||
omsetTrackerRepo: repository.NewOmsetTrackerRepository(a.db),
|
||||
rewardRepo: repository.NewRewardRepository(a.db),
|
||||
campaignRepo: repository.NewCampaignRepository(a.db),
|
||||
txManager: repository.NewTxManager(a.db),
|
||||
}
|
||||
}
|
||||
@ -252,6 +260,8 @@ type processors struct {
|
||||
gamePrizeProcessor *processor.GamePrizeProcessor
|
||||
gamePlayProcessor *processor.GamePlayProcessor
|
||||
omsetTrackerProcessor *processor.OmsetTrackerProcessor
|
||||
rewardProcessor processor.RewardProcessor
|
||||
campaignProcessor processor.CampaignProcessor
|
||||
fileClient processor.FileClient
|
||||
inventoryMovementService service.InventoryMovementService
|
||||
}
|
||||
@ -292,6 +302,8 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
gamePrizeProcessor: processor.NewGamePrizeProcessor(repos.gamePrizeRepo),
|
||||
gamePlayProcessor: processor.NewGamePlayProcessor(repos.gamePlayRepo, repos.gameRepo, repos.gamePrizeRepo, repos.customerTokensRepo, repos.customerPointsRepo),
|
||||
omsetTrackerProcessor: processor.NewOmsetTrackerProcessor(repos.omsetTrackerRepo),
|
||||
rewardProcessor: processor.NewRewardProcessor(repos.rewardRepo),
|
||||
campaignProcessor: processor.NewCampaignProcessor(repos.campaignRepo),
|
||||
fileClient: fileClient,
|
||||
inventoryMovementService: inventoryMovementService,
|
||||
}
|
||||
@ -325,6 +337,8 @@ type services struct {
|
||||
accountService service.AccountService
|
||||
orderIngredientTransactionService *service.OrderIngredientTransactionService
|
||||
gamificationService service.GamificationService
|
||||
rewardService service.RewardService
|
||||
campaignService service.CampaignService
|
||||
}
|
||||
|
||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
||||
@ -356,6 +370,8 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
accountService := service.NewAccountService(processors.accountProcessor)
|
||||
orderIngredientTransactionService := service.NewOrderIngredientTransactionService(processors.orderIngredientTransactionProcessor, repos.txManager)
|
||||
gamificationService := service.NewGamificationService(processors.customerPointsProcessor, processors.customerTokensProcessor, processors.tierProcessor, processors.gameProcessor, processors.gamePrizeProcessor, processors.gamePlayProcessor, processors.omsetTrackerProcessor)
|
||||
rewardService := service.NewRewardService(processors.rewardProcessor)
|
||||
campaignService := service.NewCampaignService(processors.campaignProcessor)
|
||||
|
||||
// Update order service with order ingredient transaction service
|
||||
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager)
|
||||
@ -388,6 +404,8 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
accountService: accountService,
|
||||
orderIngredientTransactionService: orderIngredientTransactionService,
|
||||
gamificationService: gamificationService,
|
||||
rewardService: rewardService,
|
||||
campaignService: campaignService,
|
||||
}
|
||||
}
|
||||
|
||||
@ -422,6 +440,8 @@ type validators struct {
|
||||
accountValidator *validator.AccountValidatorImpl
|
||||
orderIngredientTransactionValidator *validator.OrderIngredientTransactionValidatorImpl
|
||||
gamificationValidator *validator.GamificationValidatorImpl
|
||||
rewardValidator validator.RewardValidator
|
||||
campaignValidator validator.CampaignValidator
|
||||
}
|
||||
|
||||
func (a *App) initValidators() *validators {
|
||||
@ -446,5 +466,7 @@ func (a *App) initValidators() *validators {
|
||||
accountValidator: validator.NewAccountValidator().(*validator.AccountValidatorImpl),
|
||||
orderIngredientTransactionValidator: validator.NewOrderIngredientTransactionValidator().(*validator.OrderIngredientTransactionValidatorImpl),
|
||||
gamificationValidator: validator.NewGamificationValidator(),
|
||||
rewardValidator: validator.NewRewardValidator(),
|
||||
campaignValidator: validator.NewCampaignValidator(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,9 @@ const (
|
||||
GamePrizeEntity = "game_prize"
|
||||
GamePlayEntity = "game_play"
|
||||
OmsetTrackerEntity = "omset_tracker"
|
||||
RewardEntity = "reward"
|
||||
CampaignEntity = "campaign"
|
||||
CampaignRuleEntity = "campaign_rule"
|
||||
)
|
||||
|
||||
var HttpErrorMap = map[string]int{
|
||||
|
||||
150
internal/contract/campaign_contract.go
Normal file
150
internal/contract/campaign_contract.go
Normal file
@ -0,0 +1,150 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Request Contracts
|
||||
type CreateCampaignRequest struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Type string `json:"type" binding:"required,oneof=REWARD POINTS TOKENS MIXED"`
|
||||
StartDate time.Time `json:"start_date" binding:"required"`
|
||||
EndDate time.Time `json:"end_date" binding:"required"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ShowOnApp bool `json:"show_on_app"`
|
||||
Position int `json:"position" binding:"min=0"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Rules []CampaignRuleStruct `json:"rules" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
type UpdateCampaignRequest struct {
|
||||
ID uuid.UUID `json:"id" binding:"required"`
|
||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Type string `json:"type" binding:"required,oneof=REWARD POINTS TOKENS MIXED"`
|
||||
StartDate time.Time `json:"start_date" binding:"required"`
|
||||
EndDate time.Time `json:"end_date" binding:"required"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ShowOnApp bool `json:"show_on_app"`
|
||||
Position int `json:"position" binding:"min=0"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Rules []CampaignRuleStruct `json:"rules" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
type ListCampaignsRequest struct {
|
||||
Page int `form:"page" binding:"min=1"`
|
||||
Limit int `form:"limit" binding:"min=1,max=100"`
|
||||
Search string `form:"search"`
|
||||
Type string `form:"type"`
|
||||
IsActive *bool `form:"is_active"`
|
||||
ShowOnApp *bool `form:"show_on_app"`
|
||||
StartDate *time.Time `form:"start_date"`
|
||||
EndDate *time.Time `form:"end_date"`
|
||||
}
|
||||
|
||||
type GetCampaignRequest struct {
|
||||
ID uuid.UUID `uri:"id" binding:"required"`
|
||||
}
|
||||
|
||||
type DeleteCampaignRequest struct {
|
||||
ID uuid.UUID `uri:"id" binding:"required"`
|
||||
}
|
||||
|
||||
// Campaign Rule Request Contracts
|
||||
type CreateCampaignRuleRequest struct {
|
||||
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS TOKENS REWARD"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateCampaignRuleRequest struct {
|
||||
ID uuid.UUID `json:"id" binding:"required"`
|
||||
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS TOKENS REWARD"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type ListCampaignRulesRequest struct {
|
||||
Page int `form:"page" binding:"min=1"`
|
||||
Limit int `form:"limit" binding:"min=1,max=100"`
|
||||
CampaignID string `form:"campaign_id"`
|
||||
RuleType string `form:"rule_type"`
|
||||
RewardType string `form:"reward_type"`
|
||||
}
|
||||
|
||||
type GetCampaignRuleRequest struct {
|
||||
ID uuid.UUID `uri:"id" binding:"required"`
|
||||
}
|
||||
|
||||
type DeleteCampaignRuleRequest struct {
|
||||
ID uuid.UUID `uri:"id" binding:"required"`
|
||||
}
|
||||
|
||||
// Response Contracts
|
||||
type CampaignResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Type string `json:"type"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ShowOnApp bool `json:"show_on_app"`
|
||||
Position int `json:"position"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Rules []CampaignRuleResponse `json:"rules,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CampaignRuleResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CampaignID uuid.UUID `json:"campaign_id"`
|
||||
RuleType string `json:"rule_type"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ListCampaignsResponse struct {
|
||||
Campaigns []CampaignResponse `json:"campaigns"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type ListCampaignRulesResponse struct {
|
||||
Rules []CampaignRuleResponse `json:"rules"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// Helper structs
|
||||
type CampaignRuleStruct struct {
|
||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS TOKENS REWARD"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
82
internal/contract/reward_contract.go
Normal file
82
internal/contract/reward_contract.go
Normal file
@ -0,0 +1,82 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Request Contracts
|
||||
type CreateRewardRequest struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=VOUCHER PHYSICAL DIGITAL"`
|
||||
CostPoints int64 `json:"cost_points" binding:"required,min=1"`
|
||||
Stock *int `json:"stock,omitempty"`
|
||||
MaxPerCustomer int `json:"max_per_customer" binding:"min=1"`
|
||||
Tnc *TermsAndConditionsStruct `json:"tnc,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Images *[]string `json:"images,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateRewardRequest struct {
|
||||
ID uuid.UUID `json:"id" binding:"required"`
|
||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=VOUCHER PHYSICAL DIGITAL BALANCE"`
|
||||
CostPoints int64 `json:"cost_points" binding:"required,min=1"`
|
||||
Stock *int `json:"stock,omitempty"`
|
||||
MaxPerCustomer int `json:"max_per_customer" binding:"min=1"`
|
||||
Tnc *TermsAndConditionsStruct `json:"tnc,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Images *[]string `json:"images,omitempty"`
|
||||
}
|
||||
|
||||
type ListRewardsRequest struct {
|
||||
Page int `form:"page" binding:"min=1"`
|
||||
Limit int `form:"limit" binding:"min=1,max=100"`
|
||||
Search string `form:"search"`
|
||||
RewardType string `form:"reward_type"`
|
||||
MinPoints *int64 `form:"min_points"`
|
||||
MaxPoints *int64 `form:"max_points"`
|
||||
HasStock *bool `form:"has_stock"`
|
||||
}
|
||||
|
||||
type GetRewardRequest struct {
|
||||
ID uuid.UUID `uri:"id" binding:"required"`
|
||||
}
|
||||
|
||||
type DeleteRewardRequest struct {
|
||||
ID uuid.UUID `uri:"id" binding:"required"`
|
||||
}
|
||||
|
||||
// Response Contracts
|
||||
type RewardResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
RewardType string `json:"reward_type"`
|
||||
CostPoints int64 `json:"cost_points"`
|
||||
Stock *int `json:"stock,omitempty"`
|
||||
MaxPerCustomer int `json:"max_per_customer"`
|
||||
Tnc *TermsAndConditionsStruct `json:"tnc,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Images *[]string `json:"images,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ListRewardsResponse struct {
|
||||
Rewards []RewardResponse `json:"rewards"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// Helper structs
|
||||
type TermsAndConditionsStruct struct {
|
||||
Sections []TncSectionStruct `json:"sections"`
|
||||
ExpiryDays int `json:"expiry_days"`
|
||||
}
|
||||
|
||||
type TncSectionStruct struct {
|
||||
Title string `json:"title"`
|
||||
Rules []string `json:"rules"`
|
||||
}
|
||||
131
internal/entities/campaign.go
Normal file
131
internal/entities/campaign.go
Normal file
@ -0,0 +1,131 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CampaignType string
|
||||
|
||||
const (
|
||||
CampaignTypeReward CampaignType = "REWARD"
|
||||
CampaignTypePoints CampaignType = "POINTS"
|
||||
CampaignTypeTokens CampaignType = "TOKENS"
|
||||
CampaignTypeMixed CampaignType = "MIXED"
|
||||
)
|
||||
|
||||
type RuleType string
|
||||
|
||||
const (
|
||||
RuleTypeTier RuleType = "TIER"
|
||||
RuleTypeSpend RuleType = "SPEND"
|
||||
RuleTypeProduct RuleType = "PRODUCT"
|
||||
RuleTypeCategory RuleType = "CATEGORY"
|
||||
RuleTypeDay RuleType = "DAY"
|
||||
RuleTypeLocation RuleType = "LOCATION"
|
||||
)
|
||||
|
||||
type CampaignRewardType string
|
||||
|
||||
const (
|
||||
CampaignRewardTypePoints CampaignRewardType = "POINTS"
|
||||
CampaignRewardTypeTokens CampaignRewardType = "TOKENS"
|
||||
CampaignRewardTypeReward CampaignRewardType = "REWARD"
|
||||
)
|
||||
|
||||
type RewardSubtype string
|
||||
|
||||
const (
|
||||
RewardSubtypeMultiplier RewardSubtype = "MULTIPLIER"
|
||||
RewardSubtypeSpin RewardSubtype = "SPIN"
|
||||
RewardSubtypeBonus RewardSubtype = "BONUS"
|
||||
RewardSubtypePhysical RewardSubtype = "PHYSICAL"
|
||||
)
|
||||
|
||||
type Campaign struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
Name string `gorm:"type:varchar(150);not null" json:"name"`
|
||||
Description *string `gorm:"type:text" json:"description,omitempty"`
|
||||
Type CampaignType `gorm:"type:varchar(50);not null" json:"type"`
|
||||
StartDate time.Time `gorm:"type:timestamp;not null" json:"start_date"`
|
||||
EndDate time.Time `gorm:"type:timestamp;not null" json:"end_date"`
|
||||
IsActive bool `gorm:"type:boolean;default:true" json:"is_active"`
|
||||
ShowOnApp bool `gorm:"type:boolean;default:true" json:"show_on_app"`
|
||||
Position int `gorm:"type:int;default:0" json:"position"`
|
||||
Metadata *map[string]interface{} `gorm:"type:jsonb" json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `gorm:"type:timestamp;default:now()" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"type:timestamp;default:now()" json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Rules []CampaignRule `gorm:"foreignKey:CampaignID;constraint:OnDelete:CASCADE" json:"rules,omitempty"`
|
||||
}
|
||||
|
||||
func (Campaign) TableName() string {
|
||||
return "campaigns"
|
||||
}
|
||||
|
||||
func (c *Campaign) BeforeCreate(tx *gorm.DB) error {
|
||||
if c.ID == uuid.Nil {
|
||||
c.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Campaign) BeforeUpdate(tx *gorm.DB) error {
|
||||
c.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
type CampaignRule struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
CampaignID uuid.UUID `gorm:"type:uuid;not null" json:"campaign_id"`
|
||||
RuleType RuleType `gorm:"type:varchar(50);not null" json:"rule_type"`
|
||||
ConditionValue *string `gorm:"type:varchar(255)" json:"condition_value,omitempty"`
|
||||
RewardType CampaignRewardType `gorm:"type:varchar(50);not null" json:"reward_type"`
|
||||
RewardValue *int64 `gorm:"type:bigint" json:"reward_value,omitempty"`
|
||||
RewardSubtype *RewardSubtype `gorm:"type:varchar(50)" json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `gorm:"type:uuid" json:"reward_ref_id,omitempty"`
|
||||
Metadata *map[string]interface{} `gorm:"type:jsonb" json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `gorm:"type:timestamp;default:now()" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"type:timestamp;default:now()" json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Campaign Campaign `gorm:"foreignKey:CampaignID" json:"campaign,omitempty"`
|
||||
}
|
||||
|
||||
func (CampaignRule) TableName() string {
|
||||
return "campaign_rules"
|
||||
}
|
||||
|
||||
func (cr *CampaignRule) BeforeCreate(tx *gorm.DB) error {
|
||||
if cr.ID == uuid.Nil {
|
||||
cr.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr *CampaignRule) BeforeUpdate(tx *gorm.DB) error {
|
||||
cr.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
type ListCampaignsRequest struct {
|
||||
Page int `form:"page" binding:"min=1"`
|
||||
Limit int `form:"limit" binding:"min=1,max=100"`
|
||||
Search string `form:"search"`
|
||||
Type string `form:"type"`
|
||||
IsActive *bool `form:"is_active"`
|
||||
ShowOnApp *bool `form:"show_on_app"`
|
||||
StartDate *time.Time `form:"start_date"`
|
||||
EndDate *time.Time `form:"end_date"`
|
||||
}
|
||||
|
||||
type ListCampaignRulesRequest struct {
|
||||
Page int `form:"page" binding:"min=1"`
|
||||
Limit int `form:"limit" binding:"min=1,max=100"`
|
||||
CampaignID string `form:"campaign_id"`
|
||||
RuleType string `form:"rule_type"`
|
||||
RewardType string `form:"reward_type"`
|
||||
}
|
||||
@ -31,6 +31,9 @@ func GetAllEntities() []interface{} {
|
||||
&GamePrize{},
|
||||
&GamePlay{},
|
||||
&OmsetTracker{},
|
||||
&Reward{},
|
||||
&Campaign{},
|
||||
&CampaignRule{},
|
||||
// Analytics entities are not database tables, they are query results
|
||||
}
|
||||
}
|
||||
|
||||
67
internal/entities/reward.go
Normal file
67
internal/entities/reward.go
Normal file
@ -0,0 +1,67 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RewardType string
|
||||
|
||||
const (
|
||||
RewardTypeVoucher RewardType = "VOUCHER"
|
||||
RewardTypePhysical RewardType = "PHYSICAL"
|
||||
RewardTypeDigital RewardType = "DIGITAL"
|
||||
RewardTypeBalance RewardType = "BALANCE"
|
||||
)
|
||||
|
||||
type TermsAndConditions struct {
|
||||
Sections []TncSection `json:"sections"`
|
||||
ExpiryDays int `json:"expiry_days"`
|
||||
}
|
||||
|
||||
type TncSection struct {
|
||||
Title string `json:"title"`
|
||||
Rules []string `json:"rules"`
|
||||
}
|
||||
|
||||
type Reward struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
Name string `gorm:"type:varchar(150);not null" json:"name"`
|
||||
RewardType RewardType `gorm:"type:varchar(50);not null" json:"reward_type"`
|
||||
CostPoints int64 `gorm:"type:bigint;not null" json:"cost_points"`
|
||||
Stock *int `gorm:"type:int" json:"stock,omitempty"`
|
||||
MaxPerCustomer int `gorm:"type:int;default:1" json:"max_per_customer"`
|
||||
Tnc *TermsAndConditions `gorm:"type:jsonb" json:"tnc,omitempty"`
|
||||
Metadata *map[string]interface{} `gorm:"type:jsonb" json:"metadata,omitempty"`
|
||||
Images *[]string `gorm:"type:jsonb" json:"images,omitempty"`
|
||||
CreatedAt time.Time `gorm:"type:timestamp;default:now()" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"type:timestamp;default:now()" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (Reward) TableName() string {
|
||||
return "rewards"
|
||||
}
|
||||
|
||||
func (r *Reward) BeforeCreate(tx *gorm.DB) error {
|
||||
if r.ID == uuid.Nil {
|
||||
r.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reward) BeforeUpdate(tx *gorm.DB) error {
|
||||
r.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
type ListRewardsRequest struct {
|
||||
Page int `form:"page" binding:"min=1"`
|
||||
Limit int `form:"limit" binding:"min=1,max=100"`
|
||||
Search string `form:"search"`
|
||||
RewardType string `form:"reward_type"`
|
||||
MinPoints *int64 `form:"min_points"`
|
||||
MaxPoints *int64 `form:"max_points"`
|
||||
HasStock *bool `form:"has_stock"`
|
||||
}
|
||||
179
internal/handler/campaign_handler.go
Normal file
179
internal/handler/campaign_handler.go
Normal file
@ -0,0 +1,179 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
"apskel-pos-be/internal/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CampaignHandler struct {
|
||||
campaignService service.CampaignService
|
||||
campaignValidator validator.CampaignValidator
|
||||
}
|
||||
|
||||
func NewCampaignHandler(campaignService service.CampaignService, campaignValidator validator.CampaignValidator) *CampaignHandler {
|
||||
return &CampaignHandler{
|
||||
campaignService: campaignService,
|
||||
campaignValidator: campaignValidator,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CampaignHandler) CreateCampaign(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req contract.CreateCampaignRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::CreateCampaign -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::CreateCampaign")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.campaignValidator.ValidateCreateCampaignRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("CampaignHandler::CreateCampaign -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::CreateCampaign")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.campaignService.CreateCampaign(ctx, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::CreateCampaign -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CampaignEntity, err.Error())}), "CampaignHandler::CreateCampaign")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CampaignHandler::CreateCampaign")
|
||||
}
|
||||
|
||||
func (h *CampaignHandler) GetCampaign(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
if idStr == "" {
|
||||
logger.FromContext(c.Request.Context()).Error("CampaignHandler::GetCampaign -> missing ID parameter")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.CampaignEntity, "ID parameter is required")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::GetCampaign")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.campaignService.GetCampaign(ctx, idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::GetCampaign -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CampaignEntity, err.Error())}), "CampaignHandler::GetCampaign")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CampaignHandler::GetCampaign")
|
||||
}
|
||||
|
||||
func (h *CampaignHandler) ListCampaigns(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req contract.ListCampaignsRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::ListCampaigns -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::ListCampaigns")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.campaignValidator.ValidateListCampaignsRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("CampaignHandler::ListCampaigns -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::ListCampaigns")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.campaignService.ListCampaigns(ctx, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::ListCampaigns -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CampaignEntity, err.Error())}), "CampaignHandler::ListCampaigns")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CampaignHandler::ListCampaigns")
|
||||
}
|
||||
|
||||
func (h *CampaignHandler) UpdateCampaign(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req contract.UpdateCampaignRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::UpdateCampaign -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::UpdateCampaign")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.campaignValidator.ValidateUpdateCampaignRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("CampaignHandler::UpdateCampaign -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::UpdateCampaign")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.campaignService.UpdateCampaign(ctx, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::UpdateCampaign -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CampaignEntity, err.Error())}), "CampaignHandler::UpdateCampaign")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CampaignHandler::UpdateCampaign")
|
||||
}
|
||||
|
||||
func (h *CampaignHandler) DeleteCampaign(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
if idStr == "" {
|
||||
logger.FromContext(c.Request.Context()).Error("CampaignHandler::DeleteCampaign -> missing ID parameter")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.CampaignEntity, "ID parameter is required")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::DeleteCampaign")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.campaignService.DeleteCampaign(ctx, idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::DeleteCampaign -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CampaignEntity, err.Error())}), "CampaignHandler::DeleteCampaign")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse("Campaign deleted successfully"), "CampaignHandler::DeleteCampaign")
|
||||
}
|
||||
|
||||
func (h *CampaignHandler) GetActiveCampaigns(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
response, err := h.campaignService.GetActiveCampaigns(ctx)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::GetActiveCampaigns -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CampaignEntity, err.Error())}), "CampaignHandler::GetActiveCampaigns")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CampaignHandler::GetActiveCampaigns")
|
||||
}
|
||||
|
||||
func (h *CampaignHandler) GetCampaignsForApp(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
response, err := h.campaignService.GetCampaignsForApp(ctx)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::GetCampaignsForApp -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CampaignEntity, err.Error())}), "CampaignHandler::GetCampaignsForApp")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CampaignHandler::GetCampaignsForApp")
|
||||
}
|
||||
206
internal/handler/reward_handler.go
Normal file
206
internal/handler/reward_handler.go
Normal file
@ -0,0 +1,206 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
"apskel-pos-be/internal/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RewardHandler struct {
|
||||
rewardService service.RewardService
|
||||
rewardValidator validator.RewardValidator
|
||||
}
|
||||
|
||||
func NewRewardHandler(rewardService service.RewardService, rewardValidator validator.RewardValidator) *RewardHandler {
|
||||
return &RewardHandler{
|
||||
rewardService: rewardService,
|
||||
rewardValidator: rewardValidator,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RewardHandler) CreateReward(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req contract.CreateRewardRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("RewardHandler::CreateReward -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "RewardHandler::CreateReward")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.rewardValidator.ValidateCreateRewardRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("RewardHandler::CreateReward -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "RewardHandler::CreateReward")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.rewardService.CreateReward(ctx, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("RewardHandler::CreateReward -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.RewardEntity, err.Error())}), "RewardHandler::CreateReward")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "RewardHandler::CreateReward")
|
||||
}
|
||||
|
||||
func (h *RewardHandler) GetReward(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
if idStr == "" {
|
||||
logger.FromContext(c.Request.Context()).Error("RewardHandler::GetReward -> missing ID parameter")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RewardEntity, "ID parameter is required")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "RewardHandler::GetReward")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.rewardService.GetReward(ctx, idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("RewardHandler::GetReward -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.RewardEntity, err.Error())}), "RewardHandler::GetReward")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "RewardHandler::GetReward")
|
||||
}
|
||||
|
||||
func (h *RewardHandler) ListRewards(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req contract.ListRewardsRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("RewardHandler::ListRewards -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "RewardHandler::ListRewards")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.rewardValidator.ValidateListRewardsRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("RewardHandler::ListRewards -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "RewardHandler::ListRewards")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.rewardService.ListRewards(ctx, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("RewardHandler::ListRewards -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.RewardEntity, err.Error())}), "RewardHandler::ListRewards")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "RewardHandler::ListRewards")
|
||||
}
|
||||
|
||||
func (h *RewardHandler) UpdateReward(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req contract.UpdateRewardRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("RewardHandler::UpdateReward -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "RewardHandler::UpdateReward")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.rewardValidator.ValidateUpdateRewardRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("RewardHandler::UpdateReward -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "RewardHandler::UpdateReward")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.rewardService.UpdateReward(ctx, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("RewardHandler::UpdateReward -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.RewardEntity, err.Error())}), "RewardHandler::UpdateReward")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "RewardHandler::UpdateReward")
|
||||
}
|
||||
|
||||
func (h *RewardHandler) DeleteReward(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
if idStr == "" {
|
||||
logger.FromContext(c.Request.Context()).Error("RewardHandler::DeleteReward -> missing ID parameter")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RewardEntity, "ID parameter is required")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "RewardHandler::DeleteReward")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.rewardService.DeleteReward(ctx, idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("RewardHandler::DeleteReward -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.RewardEntity, err.Error())}), "RewardHandler::DeleteReward")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse("Reward deleted successfully"), "RewardHandler::DeleteReward")
|
||||
}
|
||||
|
||||
func (h *RewardHandler) UpdateRewardStock(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
if idStr == "" {
|
||||
logger.FromContext(c.Request.Context()).Error("RewardHandler::UpdateRewardStock -> missing ID parameter")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RewardEntity, "ID parameter is required")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "RewardHandler::UpdateRewardStock")
|
||||
return
|
||||
}
|
||||
|
||||
stockStr := c.Param("stock")
|
||||
stock, err := strconv.Atoi(stockStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("RewardHandler::UpdateRewardStock -> invalid stock parameter")
|
||||
validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.RewardEntity, "Invalid stock parameter")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "RewardHandler::UpdateRewardStock")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.rewardService.UpdateRewardStock(ctx, idStr, stock)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("RewardHandler::UpdateRewardStock -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.RewardEntity, err.Error())}), "RewardHandler::UpdateRewardStock")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse("Reward stock updated successfully"), "RewardHandler::UpdateRewardStock")
|
||||
}
|
||||
|
||||
func (h *RewardHandler) GetRewardsByType(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
rewardType := c.Param("type")
|
||||
if rewardType == "" {
|
||||
logger.FromContext(c.Request.Context()).Error("RewardHandler::GetRewardsByType -> missing type parameter")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RewardEntity, "Type parameter is required")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "RewardHandler::GetRewardsByType")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.rewardService.GetRewardsByType(ctx, rewardType)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("RewardHandler::GetRewardsByType -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.RewardEntity, err.Error())}), "RewardHandler::GetRewardsByType")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "RewardHandler::GetRewardsByType")
|
||||
}
|
||||
190
internal/mappers/campaign_mapper.go
Normal file
190
internal/mappers/campaign_mapper.go
Normal file
@ -0,0 +1,190 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func ToCampaignResponse(entity *entities.Campaign) *models.CampaignResponse {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rules []models.CampaignRuleResponse
|
||||
if entity.Rules != nil {
|
||||
for _, rule := range entity.Rules {
|
||||
rules = append(rules, *ToCampaignRuleResponse(&rule))
|
||||
}
|
||||
}
|
||||
|
||||
return &models.CampaignResponse{
|
||||
ID: entity.ID,
|
||||
Name: entity.Name,
|
||||
Description: entity.Description,
|
||||
Type: string(entity.Type),
|
||||
StartDate: entity.StartDate,
|
||||
EndDate: entity.EndDate,
|
||||
IsActive: entity.IsActive,
|
||||
ShowOnApp: entity.ShowOnApp,
|
||||
Position: entity.Position,
|
||||
Metadata: entity.Metadata,
|
||||
Rules: rules,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ToCampaignRuleResponse(entity *entities.CampaignRule) *models.CampaignRuleResponse {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rewardSubtype *string
|
||||
if entity.RewardSubtype != nil {
|
||||
subtype := string(*entity.RewardSubtype)
|
||||
rewardSubtype = &subtype
|
||||
}
|
||||
|
||||
return &models.CampaignRuleResponse{
|
||||
ID: entity.ID,
|
||||
CampaignID: entity.CampaignID,
|
||||
RuleType: string(entity.RuleType),
|
||||
ConditionValue: entity.ConditionValue,
|
||||
RewardType: string(entity.RewardType),
|
||||
RewardValue: entity.RewardValue,
|
||||
RewardSubtype: rewardSubtype,
|
||||
RewardRefID: entity.RewardRefID,
|
||||
Metadata: entity.Metadata,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ToCampaignEntity(request *contract.CreateCampaignRequest) *entities.Campaign {
|
||||
if request == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rules []entities.CampaignRule
|
||||
if request.Rules != nil {
|
||||
for _, rule := range request.Rules {
|
||||
// For create request, we'll use a temporary UUID that will be set by the repository
|
||||
rules = append(rules, *ToCampaignRuleEntity(&rule, uuid.Nil))
|
||||
}
|
||||
}
|
||||
|
||||
return &entities.Campaign{
|
||||
Name: request.Name,
|
||||
Description: request.Description,
|
||||
Type: entities.CampaignType(request.Type),
|
||||
StartDate: request.StartDate,
|
||||
EndDate: request.EndDate,
|
||||
IsActive: request.IsActive,
|
||||
ShowOnApp: request.ShowOnApp,
|
||||
Position: request.Position,
|
||||
Metadata: request.Metadata,
|
||||
Rules: rules,
|
||||
}
|
||||
}
|
||||
|
||||
func ToCampaignEntityFromUpdate(request *contract.UpdateCampaignRequest) *entities.Campaign {
|
||||
if request == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rules []entities.CampaignRule
|
||||
if request.Rules != nil {
|
||||
for _, rule := range request.Rules {
|
||||
rules = append(rules, *ToCampaignRuleEntityFromUpdate(&rule, request.ID))
|
||||
}
|
||||
}
|
||||
|
||||
return &entities.Campaign{
|
||||
ID: request.ID,
|
||||
Name: request.Name,
|
||||
Description: request.Description,
|
||||
Type: entities.CampaignType(request.Type),
|
||||
StartDate: request.StartDate,
|
||||
EndDate: request.EndDate,
|
||||
IsActive: request.IsActive,
|
||||
ShowOnApp: request.ShowOnApp,
|
||||
Position: request.Position,
|
||||
Metadata: request.Metadata,
|
||||
Rules: rules,
|
||||
}
|
||||
}
|
||||
|
||||
func ToCampaignRuleEntity(request *contract.CampaignRuleStruct, campaignID uuid.UUID) *entities.CampaignRule {
|
||||
if request == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rewardSubtype *entities.RewardSubtype
|
||||
if request.RewardSubtype != nil {
|
||||
subtype := entities.RewardSubtype(*request.RewardSubtype)
|
||||
rewardSubtype = &subtype
|
||||
}
|
||||
|
||||
return &entities.CampaignRule{
|
||||
CampaignID: campaignID,
|
||||
RuleType: entities.RuleType(request.RuleType),
|
||||
ConditionValue: request.ConditionValue,
|
||||
RewardType: entities.CampaignRewardType(request.RewardType),
|
||||
RewardValue: request.RewardValue,
|
||||
RewardSubtype: rewardSubtype,
|
||||
RewardRefID: request.RewardRefID,
|
||||
Metadata: request.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
func ToCampaignRuleEntityFromUpdate(request *contract.CampaignRuleStruct, campaignID uuid.UUID) *entities.CampaignRule {
|
||||
if request == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rewardSubtype *entities.RewardSubtype
|
||||
if request.RewardSubtype != nil {
|
||||
subtype := entities.RewardSubtype(*request.RewardSubtype)
|
||||
rewardSubtype = &subtype
|
||||
}
|
||||
|
||||
return &entities.CampaignRule{
|
||||
CampaignID: campaignID,
|
||||
RuleType: entities.RuleType(request.RuleType),
|
||||
ConditionValue: request.ConditionValue,
|
||||
RewardType: entities.CampaignRewardType(request.RewardType),
|
||||
RewardValue: request.RewardValue,
|
||||
RewardSubtype: rewardSubtype,
|
||||
RewardRefID: request.RewardRefID,
|
||||
Metadata: request.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
func ToCampaignResponseSlice(entities []entities.Campaign) []models.CampaignResponse {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
responses := make([]models.CampaignResponse, len(entities))
|
||||
for i, entity := range entities {
|
||||
responses[i] = *ToCampaignResponse(&entity)
|
||||
}
|
||||
|
||||
return responses
|
||||
}
|
||||
|
||||
func ToCampaignRuleResponseSlice(entities []entities.CampaignRule) []models.CampaignRuleResponse {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
responses := make([]models.CampaignRuleResponse, len(entities))
|
||||
for i, entity := range entities {
|
||||
responses[i] = *ToCampaignRuleResponse(&entity)
|
||||
}
|
||||
|
||||
return responses
|
||||
}
|
||||
135
internal/mappers/reward_mapper.go
Normal file
135
internal/mappers/reward_mapper.go
Normal file
@ -0,0 +1,135 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
func ToRewardResponse(entity *entities.Reward) *models.RewardResponse {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var tnc *models.TermsAndConditionsStruct
|
||||
if entity.Tnc != nil {
|
||||
var sections []models.TncSectionStruct
|
||||
for _, section := range entity.Tnc.Sections {
|
||||
sections = append(sections, models.TncSectionStruct{
|
||||
Title: section.Title,
|
||||
Rules: section.Rules,
|
||||
})
|
||||
}
|
||||
tnc = &models.TermsAndConditionsStruct{
|
||||
Sections: sections,
|
||||
ExpiryDays: entity.Tnc.ExpiryDays,
|
||||
}
|
||||
}
|
||||
|
||||
var images *[]string
|
||||
if entity.Images != nil {
|
||||
images = entity.Images
|
||||
}
|
||||
|
||||
return &models.RewardResponse{
|
||||
ID: entity.ID,
|
||||
Name: entity.Name,
|
||||
RewardType: string(entity.RewardType),
|
||||
CostPoints: entity.CostPoints,
|
||||
Stock: entity.Stock,
|
||||
MaxPerCustomer: entity.MaxPerCustomer,
|
||||
Tnc: tnc,
|
||||
Metadata: entity.Metadata,
|
||||
Images: images,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ToRewardEntity(request *contract.CreateRewardRequest) *entities.Reward {
|
||||
if request == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var tnc *entities.TermsAndConditions
|
||||
if request.Tnc != nil {
|
||||
var sections []entities.TncSection
|
||||
for _, section := range request.Tnc.Sections {
|
||||
sections = append(sections, entities.TncSection{
|
||||
Title: section.Title,
|
||||
Rules: section.Rules,
|
||||
})
|
||||
}
|
||||
tnc = &entities.TermsAndConditions{
|
||||
Sections: sections,
|
||||
ExpiryDays: request.Tnc.ExpiryDays,
|
||||
}
|
||||
}
|
||||
|
||||
var images *[]string
|
||||
if request.Images != nil {
|
||||
images = request.Images
|
||||
}
|
||||
|
||||
return &entities.Reward{
|
||||
Name: request.Name,
|
||||
RewardType: entities.RewardType(request.RewardType),
|
||||
CostPoints: request.CostPoints,
|
||||
Stock: request.Stock,
|
||||
MaxPerCustomer: request.MaxPerCustomer,
|
||||
Tnc: tnc,
|
||||
Metadata: request.Metadata,
|
||||
Images: images,
|
||||
}
|
||||
}
|
||||
|
||||
func ToRewardEntityFromUpdate(request *contract.UpdateRewardRequest) *entities.Reward {
|
||||
if request == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var tnc *entities.TermsAndConditions
|
||||
if request.Tnc != nil {
|
||||
var sections []entities.TncSection
|
||||
for _, section := range request.Tnc.Sections {
|
||||
sections = append(sections, entities.TncSection{
|
||||
Title: section.Title,
|
||||
Rules: section.Rules,
|
||||
})
|
||||
}
|
||||
tnc = &entities.TermsAndConditions{
|
||||
Sections: sections,
|
||||
ExpiryDays: request.Tnc.ExpiryDays,
|
||||
}
|
||||
}
|
||||
|
||||
var images *[]string
|
||||
if request.Images != nil {
|
||||
images = request.Images
|
||||
}
|
||||
|
||||
return &entities.Reward{
|
||||
ID: request.ID,
|
||||
Name: request.Name,
|
||||
RewardType: entities.RewardType(request.RewardType),
|
||||
CostPoints: request.CostPoints,
|
||||
Stock: request.Stock,
|
||||
MaxPerCustomer: request.MaxPerCustomer,
|
||||
Tnc: tnc,
|
||||
Metadata: request.Metadata,
|
||||
Images: images,
|
||||
}
|
||||
}
|
||||
|
||||
func ToRewardResponseSlice(entities []entities.Reward) []models.RewardResponse {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
responses := make([]models.RewardResponse, len(entities))
|
||||
for i, entity := range entities {
|
||||
responses[i] = *ToRewardResponse(&entity)
|
||||
}
|
||||
|
||||
return responses
|
||||
}
|
||||
132
internal/models/campaign.go
Normal file
132
internal/models/campaign.go
Normal file
@ -0,0 +1,132 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CreateCampaignRequest struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Type string `json:"type" binding:"required,oneof=REWARD POINTS TOKENS MIXED"`
|
||||
StartDate time.Time `json:"start_date" binding:"required"`
|
||||
EndDate time.Time `json:"end_date" binding:"required"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ShowOnApp bool `json:"show_on_app"`
|
||||
Position int `json:"position" binding:"min=0"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Rules []CampaignRuleStruct `json:"rules" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
type UpdateCampaignRequest struct {
|
||||
ID uuid.UUID `json:"id" binding:"required"`
|
||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Type string `json:"type" binding:"required,oneof=REWARD POINTS TOKENS MIXED"`
|
||||
StartDate time.Time `json:"start_date" binding:"required"`
|
||||
EndDate time.Time `json:"end_date" binding:"required"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ShowOnApp bool `json:"show_on_app"`
|
||||
Position int `json:"position" binding:"min=0"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Rules []CampaignRuleStruct `json:"rules" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
type ListCampaignsRequest struct {
|
||||
Page int `form:"page" binding:"min=1"`
|
||||
Limit int `form:"limit" binding:"min=1,max=100"`
|
||||
Search string `form:"search"`
|
||||
Type string `form:"type"`
|
||||
IsActive *bool `form:"is_active"`
|
||||
ShowOnApp *bool `form:"show_on_app"`
|
||||
StartDate *time.Time `form:"start_date"`
|
||||
EndDate *time.Time `form:"end_date"`
|
||||
}
|
||||
|
||||
type CampaignResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Type string `json:"type"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ShowOnApp bool `json:"show_on_app"`
|
||||
Position int `json:"position"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Rules []CampaignRuleResponse `json:"rules,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CampaignRuleResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CampaignID uuid.UUID `json:"campaign_id"`
|
||||
RuleType string `json:"rule_type"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ListCampaignsResponse struct {
|
||||
Campaigns []CampaignResponse `json:"campaigns"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// Campaign Rule Models
|
||||
type CreateCampaignRuleRequest struct {
|
||||
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS TOKENS REWARD"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateCampaignRuleRequest struct {
|
||||
ID uuid.UUID `json:"id" binding:"required"`
|
||||
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
|
||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS TOKENS REWARD"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type ListCampaignRulesRequest struct {
|
||||
Page int `form:"page" binding:"min=1"`
|
||||
Limit int `form:"limit" binding:"min=1,max=100"`
|
||||
CampaignID string `form:"campaign_id"`
|
||||
RuleType string `form:"rule_type"`
|
||||
RewardType string `form:"reward_type"`
|
||||
}
|
||||
|
||||
type ListCampaignRulesResponse struct {
|
||||
Rules []CampaignRuleResponse `json:"rules"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// Helper structs
|
||||
type CampaignRuleStruct struct {
|
||||
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
|
||||
ConditionValue *string `json:"condition_value,omitempty"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=POINTS TOKENS REWARD"`
|
||||
RewardValue *int64 `json:"reward_value,omitempty"`
|
||||
RewardSubtype *string `json:"reward_subtype,omitempty"`
|
||||
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
71
internal/models/reward.go
Normal file
71
internal/models/reward.go
Normal file
@ -0,0 +1,71 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CreateRewardRequest struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=VOUCHER PHYSICAL DIGITAL BALANCE"`
|
||||
CostPoints int64 `json:"cost_points" binding:"required,min=1"`
|
||||
Stock *int `json:"stock,omitempty"`
|
||||
MaxPerCustomer int `json:"max_per_customer" binding:"min=1"`
|
||||
Tnc *TermsAndConditionsStruct `json:"tnc,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Images *[]string `json:"images,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateRewardRequest struct {
|
||||
ID uuid.UUID `json:"id" binding:"required"`
|
||||
Name string `json:"name" binding:"required,min=1,max=150"`
|
||||
RewardType string `json:"reward_type" binding:"required,oneof=VOUCHER PHYSICAL DIGITAL BALANCE"`
|
||||
CostPoints int64 `json:"cost_points" binding:"required,min=1"`
|
||||
Stock *int `json:"stock,omitempty"`
|
||||
MaxPerCustomer int `json:"max_per_customer" binding:"min=1"`
|
||||
Tnc *TermsAndConditionsStruct `json:"tnc,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Images *[]string `json:"images,omitempty"`
|
||||
}
|
||||
|
||||
type ListRewardsRequest struct {
|
||||
Page int `form:"page" binding:"min=1"`
|
||||
Limit int `form:"limit" binding:"min=1,max=100"`
|
||||
Search string `form:"search"`
|
||||
RewardType string `form:"reward_type"`
|
||||
MinPoints *int64 `form:"min_points"`
|
||||
MaxPoints *int64 `form:"max_points"`
|
||||
HasStock *bool `form:"has_stock"`
|
||||
}
|
||||
|
||||
type RewardResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
RewardType string `json:"reward_type"`
|
||||
CostPoints int64 `json:"cost_points"`
|
||||
Stock *int `json:"stock,omitempty"`
|
||||
MaxPerCustomer int `json:"max_per_customer"`
|
||||
Tnc *TermsAndConditionsStruct `json:"tnc,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Images *[]string `json:"images,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ListRewardsResponse struct {
|
||||
Rewards []RewardResponse `json:"rewards"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type TermsAndConditionsStruct struct {
|
||||
Sections []TncSectionStruct `json:"sections"`
|
||||
ExpiryDays int `json:"expiry_days"`
|
||||
}
|
||||
|
||||
type TncSectionStruct struct {
|
||||
Title string `json:"title"`
|
||||
Rules []string `json:"rules"`
|
||||
}
|
||||
292
internal/processor/campaign_processor.go
Normal file
292
internal/processor/campaign_processor.go
Normal file
@ -0,0 +1,292 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
)
|
||||
|
||||
type CampaignProcessor interface {
|
||||
CreateCampaign(ctx context.Context, req *contract.CreateCampaignRequest) (*models.CampaignResponse, error)
|
||||
GetCampaign(ctx context.Context, id string) (*models.CampaignResponse, error)
|
||||
ListCampaigns(ctx context.Context, req *contract.ListCampaignsRequest) (*models.ListCampaignsResponse, error)
|
||||
UpdateCampaign(ctx context.Context, req *contract.UpdateCampaignRequest) (*models.CampaignResponse, error)
|
||||
DeleteCampaign(ctx context.Context, id string) error
|
||||
GetActiveCampaigns(ctx context.Context) ([]models.CampaignResponse, error)
|
||||
GetCampaignsForApp(ctx context.Context) ([]models.CampaignResponse, error)
|
||||
}
|
||||
|
||||
type campaignProcessor struct {
|
||||
campaignRepo repository.CampaignRepository
|
||||
}
|
||||
|
||||
func NewCampaignProcessor(campaignRepo repository.CampaignRepository) CampaignProcessor {
|
||||
return &campaignProcessor{
|
||||
campaignRepo: campaignRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *campaignProcessor) CreateCampaign(ctx context.Context, req *contract.CreateCampaignRequest) (*models.CampaignResponse, error) {
|
||||
// Convert request to entity
|
||||
entity := mappers.ToCampaignEntity(req)
|
||||
if entity == nil {
|
||||
return nil, fmt.Errorf("invalid request data")
|
||||
}
|
||||
|
||||
// Create in repository
|
||||
if err := p.campaignRepo.Create(ctx, entity); err != nil {
|
||||
return nil, fmt.Errorf("failed to create campaign: %w", err)
|
||||
}
|
||||
|
||||
// Convert entity to response
|
||||
response := mappers.ToCampaignResponse(entity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *campaignProcessor) GetCampaign(ctx context.Context, id string) (*models.CampaignResponse, error) {
|
||||
// Get from repository
|
||||
entity, err := p.campaignRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get campaign: %w", err)
|
||||
}
|
||||
|
||||
if entity == nil {
|
||||
return nil, fmt.Errorf("campaign not found")
|
||||
}
|
||||
|
||||
// Convert entity to response
|
||||
response := mappers.ToCampaignResponse(entity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *campaignProcessor) ListCampaigns(ctx context.Context, req *contract.ListCampaignsRequest) (*models.ListCampaignsResponse, error) {
|
||||
// Convert request to entity request
|
||||
entityReq := &entities.ListCampaignsRequest{
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
Search: req.Search,
|
||||
Type: req.Type,
|
||||
IsActive: req.IsActive,
|
||||
ShowOnApp: req.ShowOnApp,
|
||||
StartDate: req.StartDate,
|
||||
EndDate: req.EndDate,
|
||||
}
|
||||
|
||||
// Get from repository
|
||||
entities, total, err := p.campaignRepo.List(ctx, entityReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list campaigns: %w", err)
|
||||
}
|
||||
|
||||
// Convert entities to response
|
||||
campaigns := mappers.ToCampaignResponseSlice(entities)
|
||||
|
||||
response := &models.ListCampaignsResponse{
|
||||
Campaigns: campaigns,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *campaignProcessor) UpdateCampaign(ctx context.Context, req *contract.UpdateCampaignRequest) (*models.CampaignResponse, error) {
|
||||
// Convert request to entity
|
||||
entity := mappers.ToCampaignEntityFromUpdate(req)
|
||||
if entity == nil {
|
||||
return nil, fmt.Errorf("invalid request data")
|
||||
}
|
||||
|
||||
// Update in repository
|
||||
if err := p.campaignRepo.Update(ctx, entity); err != nil {
|
||||
return nil, fmt.Errorf("failed to update campaign: %w", err)
|
||||
}
|
||||
|
||||
// Get updated entity
|
||||
updatedEntity, err := p.campaignRepo.GetByID(ctx, req.ID.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get updated campaign: %w", err)
|
||||
}
|
||||
|
||||
// Convert entity to response
|
||||
response := mappers.ToCampaignResponse(updatedEntity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *campaignProcessor) DeleteCampaign(ctx context.Context, id string) error {
|
||||
// Delete from repository
|
||||
if err := p.campaignRepo.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete campaign: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *campaignProcessor) GetActiveCampaigns(ctx context.Context) ([]models.CampaignResponse, error) {
|
||||
// Get from repository
|
||||
entities, err := p.campaignRepo.GetActiveCampaigns(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get active campaigns: %w", err)
|
||||
}
|
||||
|
||||
// Convert entities to response
|
||||
campaigns := mappers.ToCampaignResponseSlice(entities)
|
||||
return campaigns, nil
|
||||
}
|
||||
|
||||
func (p *campaignProcessor) GetCampaignsForApp(ctx context.Context) ([]models.CampaignResponse, error) {
|
||||
// Get from repository
|
||||
entities, err := p.campaignRepo.GetCampaignsForApp(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get campaigns for app: %w", err)
|
||||
}
|
||||
|
||||
// Convert entities to response
|
||||
campaigns := mappers.ToCampaignResponseSlice(entities)
|
||||
return campaigns, nil
|
||||
}
|
||||
|
||||
// Campaign Rule Processor
|
||||
type CampaignRuleProcessor interface {
|
||||
CreateCampaignRule(ctx context.Context, req *contract.CreateCampaignRuleRequest) (*models.CampaignRuleResponse, error)
|
||||
GetCampaignRule(ctx context.Context, id string) (*models.CampaignRuleResponse, error)
|
||||
ListCampaignRules(ctx context.Context, req *contract.ListCampaignRulesRequest) (*models.ListCampaignRulesResponse, error)
|
||||
UpdateCampaignRule(ctx context.Context, req *contract.UpdateCampaignRuleRequest) (*models.CampaignRuleResponse, error)
|
||||
DeleteCampaignRule(ctx context.Context, id string) error
|
||||
GetCampaignRulesByCampaignID(ctx context.Context, campaignID string) ([]models.CampaignRuleResponse, error)
|
||||
}
|
||||
|
||||
type campaignRuleProcessor struct {
|
||||
campaignRuleRepo repository.CampaignRuleRepository
|
||||
}
|
||||
|
||||
func NewCampaignRuleProcessor(campaignRuleRepo repository.CampaignRuleRepository) CampaignRuleProcessor {
|
||||
return &campaignRuleProcessor{
|
||||
campaignRuleRepo: campaignRuleRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *campaignRuleProcessor) CreateCampaignRule(ctx context.Context, req *contract.CreateCampaignRuleRequest) (*models.CampaignRuleResponse, error) {
|
||||
// Convert request to entity
|
||||
entity := &entities.CampaignRule{
|
||||
CampaignID: req.CampaignID,
|
||||
RuleType: entities.RuleType(req.RuleType),
|
||||
ConditionValue: req.ConditionValue,
|
||||
RewardType: entities.CampaignRewardType(req.RewardType),
|
||||
RewardValue: req.RewardValue,
|
||||
RewardSubtype: (*entities.RewardSubtype)(req.RewardSubtype),
|
||||
RewardRefID: req.RewardRefID,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
// Create in repository
|
||||
if err := p.campaignRuleRepo.Create(ctx, entity); err != nil {
|
||||
return nil, fmt.Errorf("failed to create campaign rule: %w", err)
|
||||
}
|
||||
|
||||
// Convert entity to response
|
||||
response := mappers.ToCampaignRuleResponse(entity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *campaignRuleProcessor) GetCampaignRule(ctx context.Context, id string) (*models.CampaignRuleResponse, error) {
|
||||
// Get from repository
|
||||
entity, err := p.campaignRuleRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get campaign rule: %w", err)
|
||||
}
|
||||
|
||||
if entity == nil {
|
||||
return nil, fmt.Errorf("campaign rule not found")
|
||||
}
|
||||
|
||||
// Convert entity to response
|
||||
response := mappers.ToCampaignRuleResponse(entity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *campaignRuleProcessor) ListCampaignRules(ctx context.Context, req *contract.ListCampaignRulesRequest) (*models.ListCampaignRulesResponse, error) {
|
||||
// Convert request to entity request
|
||||
entityReq := &entities.ListCampaignRulesRequest{
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
CampaignID: req.CampaignID,
|
||||
RuleType: req.RuleType,
|
||||
RewardType: req.RewardType,
|
||||
}
|
||||
|
||||
// Get from repository
|
||||
entities, total, err := p.campaignRuleRepo.List(ctx, entityReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list campaign rules: %w", err)
|
||||
}
|
||||
|
||||
// Convert entities to response
|
||||
rules := mappers.ToCampaignRuleResponseSlice(entities)
|
||||
|
||||
response := &models.ListCampaignRulesResponse{
|
||||
Rules: rules,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *campaignRuleProcessor) UpdateCampaignRule(ctx context.Context, req *contract.UpdateCampaignRuleRequest) (*models.CampaignRuleResponse, error) {
|
||||
// Convert request to entity
|
||||
entity := &entities.CampaignRule{
|
||||
ID: req.ID,
|
||||
CampaignID: req.CampaignID,
|
||||
RuleType: entities.RuleType(req.RuleType),
|
||||
ConditionValue: req.ConditionValue,
|
||||
RewardType: entities.CampaignRewardType(req.RewardType),
|
||||
RewardValue: req.RewardValue,
|
||||
RewardSubtype: (*entities.RewardSubtype)(req.RewardSubtype),
|
||||
RewardRefID: req.RewardRefID,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
// Update in repository
|
||||
if err := p.campaignRuleRepo.Update(ctx, entity); err != nil {
|
||||
return nil, fmt.Errorf("failed to update campaign rule: %w", err)
|
||||
}
|
||||
|
||||
// Get updated entity
|
||||
updatedEntity, err := p.campaignRuleRepo.GetByID(ctx, req.ID.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get updated campaign rule: %w", err)
|
||||
}
|
||||
|
||||
// Convert entity to response
|
||||
response := mappers.ToCampaignRuleResponse(updatedEntity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *campaignRuleProcessor) DeleteCampaignRule(ctx context.Context, id string) error {
|
||||
// Delete from repository
|
||||
if err := p.campaignRuleRepo.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete campaign rule: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *campaignRuleProcessor) GetCampaignRulesByCampaignID(ctx context.Context, campaignID string) ([]models.CampaignRuleResponse, error) {
|
||||
// Get from repository
|
||||
entities, err := p.campaignRuleRepo.GetByCampaignID(ctx, campaignID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get campaign rules by campaign ID: %w", err)
|
||||
}
|
||||
|
||||
// Convert entities to response
|
||||
rules := mappers.ToCampaignRuleResponseSlice(entities)
|
||||
return rules, nil
|
||||
}
|
||||
149
internal/processor/reward_processor.go
Normal file
149
internal/processor/reward_processor.go
Normal file
@ -0,0 +1,149 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
)
|
||||
|
||||
type RewardProcessor interface {
|
||||
CreateReward(ctx context.Context, req *contract.CreateRewardRequest) (*models.RewardResponse, error)
|
||||
GetReward(ctx context.Context, id string) (*models.RewardResponse, error)
|
||||
ListRewards(ctx context.Context, req *contract.ListRewardsRequest) (*models.ListRewardsResponse, error)
|
||||
UpdateReward(ctx context.Context, req *contract.UpdateRewardRequest) (*models.RewardResponse, error)
|
||||
DeleteReward(ctx context.Context, id string) error
|
||||
UpdateRewardStock(ctx context.Context, id string, newStock int) error
|
||||
GetRewardsByType(ctx context.Context, rewardType string) ([]models.RewardResponse, error)
|
||||
}
|
||||
|
||||
type rewardProcessor struct {
|
||||
rewardRepo repository.RewardRepository
|
||||
}
|
||||
|
||||
func NewRewardProcessor(rewardRepo repository.RewardRepository) RewardProcessor {
|
||||
return &rewardProcessor{
|
||||
rewardRepo: rewardRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *rewardProcessor) CreateReward(ctx context.Context, req *contract.CreateRewardRequest) (*models.RewardResponse, error) {
|
||||
// Convert request to entity
|
||||
entity := mappers.ToRewardEntity(req)
|
||||
if entity == nil {
|
||||
return nil, fmt.Errorf("invalid request data")
|
||||
}
|
||||
|
||||
// Create in repository
|
||||
if err := p.rewardRepo.Create(ctx, entity); err != nil {
|
||||
return nil, fmt.Errorf("failed to create reward: %w", err)
|
||||
}
|
||||
|
||||
// Convert entity to response
|
||||
response := mappers.ToRewardResponse(entity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *rewardProcessor) GetReward(ctx context.Context, id string) (*models.RewardResponse, error) {
|
||||
// Get from repository
|
||||
entity, err := p.rewardRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get reward: %w", err)
|
||||
}
|
||||
|
||||
if entity == nil {
|
||||
return nil, fmt.Errorf("reward not found")
|
||||
}
|
||||
|
||||
// Convert entity to response
|
||||
response := mappers.ToRewardResponse(entity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *rewardProcessor) ListRewards(ctx context.Context, req *contract.ListRewardsRequest) (*models.ListRewardsResponse, error) {
|
||||
// Convert request to entity request
|
||||
entityReq := &entities.ListRewardsRequest{
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
Search: req.Search,
|
||||
RewardType: req.RewardType,
|
||||
MinPoints: req.MinPoints,
|
||||
MaxPoints: req.MaxPoints,
|
||||
HasStock: req.HasStock,
|
||||
}
|
||||
|
||||
// Get from repository
|
||||
entities, total, err := p.rewardRepo.List(ctx, entityReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list rewards: %w", err)
|
||||
}
|
||||
|
||||
// Convert entities to response
|
||||
rewards := mappers.ToRewardResponseSlice(entities)
|
||||
|
||||
response := &models.ListRewardsResponse{
|
||||
Rewards: rewards,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *rewardProcessor) UpdateReward(ctx context.Context, req *contract.UpdateRewardRequest) (*models.RewardResponse, error) {
|
||||
// Convert request to entity
|
||||
entity := mappers.ToRewardEntityFromUpdate(req)
|
||||
if entity == nil {
|
||||
return nil, fmt.Errorf("invalid request data")
|
||||
}
|
||||
|
||||
// Update in repository
|
||||
if err := p.rewardRepo.Update(ctx, entity); err != nil {
|
||||
return nil, fmt.Errorf("failed to update reward: %w", err)
|
||||
}
|
||||
|
||||
// Get updated entity
|
||||
updatedEntity, err := p.rewardRepo.GetByID(ctx, req.ID.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get updated reward: %w", err)
|
||||
}
|
||||
|
||||
// Convert entity to response
|
||||
response := mappers.ToRewardResponse(updatedEntity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *rewardProcessor) DeleteReward(ctx context.Context, id string) error {
|
||||
// Delete from repository
|
||||
if err := p.rewardRepo.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete reward: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *rewardProcessor) UpdateRewardStock(ctx context.Context, id string, newStock int) error {
|
||||
// Update stock in repository
|
||||
if err := p.rewardRepo.UpdateStock(ctx, id, newStock); err != nil {
|
||||
return fmt.Errorf("failed to update reward stock: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *rewardProcessor) GetRewardsByType(ctx context.Context, rewardType string) ([]models.RewardResponse, error) {
|
||||
// Get from repository
|
||||
entities, err := p.rewardRepo.GetByRewardType(ctx, entities.RewardType(rewardType))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rewards by type: %w", err)
|
||||
}
|
||||
|
||||
// Convert entities to response
|
||||
rewards := mappers.ToRewardResponseSlice(entities)
|
||||
return rewards, nil
|
||||
}
|
||||
268
internal/repository/campaign_repository.go
Normal file
268
internal/repository/campaign_repository.go
Normal file
@ -0,0 +1,268 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CampaignRepository interface {
|
||||
Create(ctx context.Context, campaign *entities.Campaign) error
|
||||
GetByID(ctx context.Context, id string) (*entities.Campaign, error)
|
||||
List(ctx context.Context, req *entities.ListCampaignsRequest) ([]entities.Campaign, int64, error)
|
||||
Update(ctx context.Context, campaign *entities.Campaign) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
GetActiveCampaigns(ctx context.Context) ([]entities.Campaign, error)
|
||||
GetCampaignsForApp(ctx context.Context) ([]entities.Campaign, error)
|
||||
}
|
||||
|
||||
type campaignRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCampaignRepository(db *gorm.DB) CampaignRepository {
|
||||
return &campaignRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *campaignRepository) Create(ctx context.Context, campaign *entities.Campaign) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// Create campaign first
|
||||
if err := tx.Create(campaign).Error; err != nil {
|
||||
return fmt.Errorf("failed to create campaign: %w", err)
|
||||
}
|
||||
|
||||
// Create campaign rules
|
||||
if len(campaign.Rules) > 0 {
|
||||
for i := range campaign.Rules {
|
||||
campaign.Rules[i].CampaignID = campaign.ID
|
||||
}
|
||||
if err := tx.Create(&campaign.Rules).Error; err != nil {
|
||||
return fmt.Errorf("failed to create campaign rules: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *campaignRepository) GetByID(ctx context.Context, id string) (*entities.Campaign, error) {
|
||||
var campaign entities.Campaign
|
||||
if err := r.db.WithContext(ctx).Preload("Rules").Where("id = ?", id).First(&campaign).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("campaign not found")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get campaign: %w", err)
|
||||
}
|
||||
return &campaign, nil
|
||||
}
|
||||
|
||||
func (r *campaignRepository) List(ctx context.Context, req *entities.ListCampaignsRequest) ([]entities.Campaign, int64, error) {
|
||||
var campaigns []entities.Campaign
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entities.Campaign{})
|
||||
|
||||
// Apply filters
|
||||
if req.Search != "" {
|
||||
query = query.Where("name ILIKE ? OR description ILIKE ?", "%"+req.Search+"%", "%"+req.Search+"%")
|
||||
}
|
||||
|
||||
if req.Type != "" {
|
||||
query = query.Where("type = ?", req.Type)
|
||||
}
|
||||
|
||||
if req.IsActive != nil {
|
||||
query = query.Where("is_active = ?", *req.IsActive)
|
||||
}
|
||||
|
||||
if req.ShowOnApp != nil {
|
||||
query = query.Where("show_on_app = ?", *req.ShowOnApp)
|
||||
}
|
||||
|
||||
if req.StartDate != nil {
|
||||
query = query.Where("start_date >= ?", *req.StartDate)
|
||||
}
|
||||
|
||||
if req.EndDate != nil {
|
||||
query = query.Where("end_date <= ?", *req.EndDate)
|
||||
}
|
||||
|
||||
// Count total records
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count campaigns: %w", err)
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
if req.Page > 0 && req.Limit > 0 {
|
||||
offset := (req.Page - 1) * req.Limit
|
||||
query = query.Offset(offset).Limit(req.Limit)
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
query = query.Order("position ASC, created_at DESC")
|
||||
|
||||
if err := query.Preload("Rules").Find(&campaigns).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list campaigns: %w", err)
|
||||
}
|
||||
|
||||
return campaigns, total, nil
|
||||
}
|
||||
|
||||
func (r *campaignRepository) Update(ctx context.Context, campaign *entities.Campaign) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// Update campaign
|
||||
if err := tx.Save(campaign).Error; err != nil {
|
||||
return fmt.Errorf("failed to update campaign: %w", err)
|
||||
}
|
||||
|
||||
// Delete existing rules
|
||||
if err := tx.Where("campaign_id = ?", campaign.ID).Delete(&entities.CampaignRule{}).Error; err != nil {
|
||||
return fmt.Errorf("failed to delete existing campaign rules: %w", err)
|
||||
}
|
||||
|
||||
// Create new rules
|
||||
if len(campaign.Rules) > 0 {
|
||||
for i := range campaign.Rules {
|
||||
campaign.Rules[i].CampaignID = campaign.ID
|
||||
}
|
||||
if err := tx.Create(&campaign.Rules).Error; err != nil {
|
||||
return fmt.Errorf("failed to create campaign rules: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *campaignRepository) Delete(ctx context.Context, id string) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// Delete campaign (rules will be deleted by CASCADE)
|
||||
if err := tx.Where("id = ?", id).Delete(&entities.Campaign{}).Error; err != nil {
|
||||
return fmt.Errorf("failed to delete campaign: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *campaignRepository) GetActiveCampaigns(ctx context.Context) ([]entities.Campaign, error) {
|
||||
var campaigns []entities.Campaign
|
||||
now := "now()"
|
||||
if err := r.db.WithContext(ctx).Where("is_active = ? AND start_date <= ? AND end_date >= ?", true, now, now).Preload("Rules").Find(&campaigns).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get active campaigns: %w", err)
|
||||
}
|
||||
return campaigns, nil
|
||||
}
|
||||
|
||||
func (r *campaignRepository) GetCampaignsForApp(ctx context.Context) ([]entities.Campaign, error) {
|
||||
var campaigns []entities.Campaign
|
||||
now := "now()"
|
||||
if err := r.db.WithContext(ctx).Where("is_active = ? AND show_on_app = ? AND start_date <= ? AND end_date >= ?", true, true, now, now).Preload("Rules").Order("position ASC").Find(&campaigns).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get campaigns for app: %w", err)
|
||||
}
|
||||
return campaigns, nil
|
||||
}
|
||||
|
||||
// Campaign Rule Repository
|
||||
type CampaignRuleRepository interface {
|
||||
Create(ctx context.Context, rule *entities.CampaignRule) error
|
||||
GetByID(ctx context.Context, id string) (*entities.CampaignRule, error)
|
||||
List(ctx context.Context, req *entities.ListCampaignRulesRequest) ([]entities.CampaignRule, int64, error)
|
||||
Update(ctx context.Context, rule *entities.CampaignRule) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
GetByCampaignID(ctx context.Context, campaignID string) ([]entities.CampaignRule, error)
|
||||
}
|
||||
|
||||
type campaignRuleRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCampaignRuleRepository(db *gorm.DB) CampaignRuleRepository {
|
||||
return &campaignRuleRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *campaignRuleRepository) Create(ctx context.Context, rule *entities.CampaignRule) error {
|
||||
if err := r.db.WithContext(ctx).Create(rule).Error; err != nil {
|
||||
return fmt.Errorf("failed to create campaign rule: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *campaignRuleRepository) GetByID(ctx context.Context, id string) (*entities.CampaignRule, error) {
|
||||
var rule entities.CampaignRule
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&rule).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("campaign rule not found")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get campaign rule: %w", err)
|
||||
}
|
||||
return &rule, nil
|
||||
}
|
||||
|
||||
func (r *campaignRuleRepository) List(ctx context.Context, req *entities.ListCampaignRulesRequest) ([]entities.CampaignRule, int64, error) {
|
||||
var rules []entities.CampaignRule
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entities.CampaignRule{})
|
||||
|
||||
// Apply filters
|
||||
if req.CampaignID != "" {
|
||||
query = query.Where("campaign_id = ?", req.CampaignID)
|
||||
}
|
||||
|
||||
if req.RuleType != "" {
|
||||
query = query.Where("rule_type = ?", req.RuleType)
|
||||
}
|
||||
|
||||
if req.RewardType != "" {
|
||||
query = query.Where("reward_type = ?", req.RewardType)
|
||||
}
|
||||
|
||||
// Count total records
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count campaign rules: %w", err)
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
if req.Page > 0 && req.Limit > 0 {
|
||||
offset := (req.Page - 1) * req.Limit
|
||||
query = query.Offset(offset).Limit(req.Limit)
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
query = query.Order("created_at DESC")
|
||||
|
||||
if err := query.Find(&rules).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list campaign rules: %w", err)
|
||||
}
|
||||
|
||||
return rules, total, nil
|
||||
}
|
||||
|
||||
func (r *campaignRuleRepository) Update(ctx context.Context, rule *entities.CampaignRule) error {
|
||||
if err := r.db.WithContext(ctx).Save(rule).Error; err != nil {
|
||||
return fmt.Errorf("failed to update campaign rule: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *campaignRuleRepository) Delete(ctx context.Context, id string) error {
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&entities.CampaignRule{}).Error; err != nil {
|
||||
return fmt.Errorf("failed to delete campaign rule: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *campaignRuleRepository) GetByCampaignID(ctx context.Context, campaignID string) ([]entities.CampaignRule, error) {
|
||||
var rules []entities.CampaignRule
|
||||
if err := r.db.WithContext(ctx).Where("campaign_id = ?", campaignID).Find(&rules).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get campaign rules by campaign ID: %w", err)
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
129
internal/repository/reward_repository.go
Normal file
129
internal/repository/reward_repository.go
Normal file
@ -0,0 +1,129 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RewardRepository interface {
|
||||
Create(ctx context.Context, reward *entities.Reward) error
|
||||
GetByID(ctx context.Context, id string) (*entities.Reward, error)
|
||||
List(ctx context.Context, req *entities.ListRewardsRequest) ([]entities.Reward, int64, error)
|
||||
Update(ctx context.Context, reward *entities.Reward) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
UpdateStock(ctx context.Context, id string, newStock int) error
|
||||
GetByRewardType(ctx context.Context, rewardType entities.RewardType) ([]entities.Reward, error)
|
||||
}
|
||||
|
||||
type rewardRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRewardRepository(db *gorm.DB) RewardRepository {
|
||||
return &rewardRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rewardRepository) Create(ctx context.Context, reward *entities.Reward) error {
|
||||
if err := r.db.WithContext(ctx).Create(reward).Error; err != nil {
|
||||
return fmt.Errorf("failed to create reward: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rewardRepository) GetByID(ctx context.Context, id string) (*entities.Reward, error) {
|
||||
var reward entities.Reward
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&reward).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("reward not found")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get reward: %w", err)
|
||||
}
|
||||
return &reward, nil
|
||||
}
|
||||
|
||||
func (r *rewardRepository) List(ctx context.Context, req *entities.ListRewardsRequest) ([]entities.Reward, int64, error) {
|
||||
var rewards []entities.Reward
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entities.Reward{})
|
||||
|
||||
// Apply filters
|
||||
if req.Search != "" {
|
||||
query = query.Where("name ILIKE ?", "%"+req.Search+"%")
|
||||
}
|
||||
|
||||
if req.RewardType != "" {
|
||||
query = query.Where("reward_type = ?", req.RewardType)
|
||||
}
|
||||
|
||||
if req.MinPoints != nil {
|
||||
query = query.Where("cost_points >= ?", *req.MinPoints)
|
||||
}
|
||||
|
||||
if req.MaxPoints != nil {
|
||||
query = query.Where("cost_points <= ?", *req.MaxPoints)
|
||||
}
|
||||
|
||||
if req.HasStock != nil {
|
||||
if *req.HasStock {
|
||||
query = query.Where("stock > 0 OR stock IS NULL")
|
||||
} else {
|
||||
query = query.Where("stock <= 0")
|
||||
}
|
||||
}
|
||||
|
||||
// Count total records
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count rewards: %w", err)
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
if req.Page > 0 && req.Limit > 0 {
|
||||
offset := (req.Page - 1) * req.Limit
|
||||
query = query.Offset(offset).Limit(req.Limit)
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
query = query.Order("created_at DESC")
|
||||
|
||||
if err := query.Find(&rewards).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list rewards: %w", err)
|
||||
}
|
||||
|
||||
return rewards, total, nil
|
||||
}
|
||||
|
||||
func (r *rewardRepository) Update(ctx context.Context, reward *entities.Reward) error {
|
||||
if err := r.db.WithContext(ctx).Save(reward).Error; err != nil {
|
||||
return fmt.Errorf("failed to update reward: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rewardRepository) Delete(ctx context.Context, id string) error {
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&entities.Reward{}).Error; err != nil {
|
||||
return fmt.Errorf("failed to delete reward: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rewardRepository) UpdateStock(ctx context.Context, id string, newStock int) error {
|
||||
if err := r.db.WithContext(ctx).Model(&entities.Reward{}).Where("id = ?", id).Update("stock", newStock).Error; err != nil {
|
||||
return fmt.Errorf("failed to update reward stock: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rewardRepository) GetByRewardType(ctx context.Context, rewardType entities.RewardType) ([]entities.Reward, error) {
|
||||
var rewards []entities.Reward
|
||||
if err := r.db.WithContext(ctx).Where("reward_type = ?", rewardType).Find(&rewards).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get rewards by type: %w", err)
|
||||
}
|
||||
return rewards, nil
|
||||
}
|
||||
@ -41,6 +41,8 @@ type Router struct {
|
||||
accountHandler *handler.AccountHandler
|
||||
orderIngredientTransactionHandler *handler.OrderIngredientTransactionHandler
|
||||
gamificationHandler *handler.GamificationHandler
|
||||
rewardHandler *handler.RewardHandler
|
||||
campaignHandler *handler.CampaignHandler
|
||||
authMiddleware *middleware.AuthMiddleware
|
||||
}
|
||||
|
||||
@ -93,7 +95,11 @@ func NewRouter(cfg *config.Config,
|
||||
orderIngredientTransactionService service.OrderIngredientTransactionService,
|
||||
orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator,
|
||||
gamificationService service.GamificationService,
|
||||
gamificationValidator validator.GamificationValidator) *Router {
|
||||
gamificationValidator validator.GamificationValidator,
|
||||
rewardService service.RewardService,
|
||||
rewardValidator validator.RewardValidator,
|
||||
campaignService service.CampaignService,
|
||||
campaignValidator validator.CampaignValidator) *Router {
|
||||
|
||||
return &Router{
|
||||
config: cfg,
|
||||
@ -124,6 +130,8 @@ func NewRouter(cfg *config.Config,
|
||||
accountHandler: handler.NewAccountHandler(accountService, accountValidator),
|
||||
orderIngredientTransactionHandler: handler.NewOrderIngredientTransactionHandler(&orderIngredientTransactionService, orderIngredientTransactionValidator),
|
||||
gamificationHandler: handler.NewGamificationHandler(gamificationService, gamificationValidator),
|
||||
rewardHandler: handler.NewRewardHandler(rewardService, rewardValidator),
|
||||
campaignHandler: handler.NewCampaignHandler(campaignService, campaignValidator),
|
||||
authMiddleware: authMiddleware,
|
||||
}
|
||||
}
|
||||
@ -510,6 +518,30 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
// omsetTracker.PUT("/:id", r.gamificationHandler.UpdateOmsetTracker)
|
||||
// omsetTracker.DELETE("/:id", r.gamificationHandler.DeleteOmsetTracker)
|
||||
//}
|
||||
|
||||
// Rewards
|
||||
rewards := gamification.Group("/rewards")
|
||||
{
|
||||
rewards.POST("", r.rewardHandler.CreateReward)
|
||||
rewards.GET("", r.rewardHandler.ListRewards)
|
||||
rewards.GET("/:id", r.rewardHandler.GetReward)
|
||||
rewards.PUT("/:id", r.rewardHandler.UpdateReward)
|
||||
rewards.DELETE("/:id", r.rewardHandler.DeleteReward)
|
||||
rewards.PUT("/:id/stock/:stock", r.rewardHandler.UpdateRewardStock)
|
||||
rewards.GET("/type/:type", r.rewardHandler.GetRewardsByType)
|
||||
}
|
||||
|
||||
// Campaigns
|
||||
campaigns := gamification.Group("/campaigns")
|
||||
{
|
||||
campaigns.POST("", r.campaignHandler.CreateCampaign)
|
||||
campaigns.GET("", r.campaignHandler.ListCampaigns)
|
||||
campaigns.GET("/active", r.campaignHandler.GetActiveCampaigns)
|
||||
campaigns.GET("/app", r.campaignHandler.GetCampaignsForApp)
|
||||
campaigns.GET("/:id", r.campaignHandler.GetCampaign)
|
||||
campaigns.PUT("/:id", r.campaignHandler.UpdateCampaign)
|
||||
campaigns.DELETE("/:id", r.campaignHandler.DeleteCampaign)
|
||||
}
|
||||
}
|
||||
|
||||
outlets := protected.Group("/outlets")
|
||||
|
||||
245
internal/service/campaign_service.go
Normal file
245
internal/service/campaign_service.go
Normal file
@ -0,0 +1,245 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/processor"
|
||||
)
|
||||
|
||||
type CampaignService interface {
|
||||
CreateCampaign(ctx context.Context, req *contract.CreateCampaignRequest) (*models.CampaignResponse, error)
|
||||
GetCampaign(ctx context.Context, id string) (*models.CampaignResponse, error)
|
||||
ListCampaigns(ctx context.Context, req *contract.ListCampaignsRequest) (*models.ListCampaignsResponse, error)
|
||||
UpdateCampaign(ctx context.Context, req *contract.UpdateCampaignRequest) (*models.CampaignResponse, error)
|
||||
DeleteCampaign(ctx context.Context, id string) error
|
||||
GetActiveCampaigns(ctx context.Context) ([]models.CampaignResponse, error)
|
||||
GetCampaignsForApp(ctx context.Context) ([]models.CampaignResponse, error)
|
||||
}
|
||||
|
||||
type campaignService struct {
|
||||
campaignProcessor processor.CampaignProcessor
|
||||
}
|
||||
|
||||
func NewCampaignService(campaignProcessor processor.CampaignProcessor) CampaignService {
|
||||
return &campaignService{
|
||||
campaignProcessor: campaignProcessor,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *campaignService) CreateCampaign(ctx context.Context, req *contract.CreateCampaignRequest) (*models.CampaignResponse, error) {
|
||||
// Validate campaign type
|
||||
if err := s.validateCampaignType(req.Type); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Validate date range
|
||||
if err := s.validateDateRange(req.StartDate, req.EndDate); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Validate rules
|
||||
if err := s.validateCampaignRules(req.Rules); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Validate position
|
||||
if req.Position < 0 {
|
||||
return nil, fmt.Errorf("position cannot be negative")
|
||||
}
|
||||
|
||||
// Create campaign
|
||||
response, err := s.campaignProcessor.CreateCampaign(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create campaign: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *campaignService) GetCampaign(ctx context.Context, id string) (*models.CampaignResponse, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("campaign ID is required")
|
||||
}
|
||||
|
||||
response, err := s.campaignProcessor.GetCampaign(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get campaign: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *campaignService) ListCampaigns(ctx context.Context, req *contract.ListCampaignsRequest) (*models.ListCampaignsResponse, error) {
|
||||
// Set default pagination
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.Limit <= 0 {
|
||||
req.Limit = 10
|
||||
}
|
||||
if req.Limit > 100 {
|
||||
req.Limit = 100
|
||||
}
|
||||
|
||||
// Validate campaign type filter if provided
|
||||
if req.Type != "" {
|
||||
if err := s.validateCampaignType(req.Type); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
response, err := s.campaignProcessor.ListCampaigns(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list campaigns: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *campaignService) UpdateCampaign(ctx context.Context, req *contract.UpdateCampaignRequest) (*models.CampaignResponse, error) {
|
||||
if req.ID.String() == "" {
|
||||
return nil, fmt.Errorf("campaign ID is required")
|
||||
}
|
||||
|
||||
// Validate campaign type
|
||||
if err := s.validateCampaignType(req.Type); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Validate date range
|
||||
if err := s.validateDateRange(req.StartDate, req.EndDate); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Validate rules
|
||||
if err := s.validateCampaignRules(req.Rules); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Validate position
|
||||
if req.Position < 0 {
|
||||
return nil, fmt.Errorf("position cannot be negative")
|
||||
}
|
||||
|
||||
response, err := s.campaignProcessor.UpdateCampaign(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update campaign: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *campaignService) DeleteCampaign(ctx context.Context, id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("campaign ID is required")
|
||||
}
|
||||
|
||||
err := s.campaignProcessor.DeleteCampaign(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete campaign: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *campaignService) GetActiveCampaigns(ctx context.Context) ([]models.CampaignResponse, error) {
|
||||
campaigns, err := s.campaignProcessor.GetActiveCampaigns(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get active campaigns: %w", err)
|
||||
}
|
||||
|
||||
return campaigns, nil
|
||||
}
|
||||
|
||||
func (s *campaignService) GetCampaignsForApp(ctx context.Context) ([]models.CampaignResponse, error) {
|
||||
campaigns, err := s.campaignProcessor.GetCampaignsForApp(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get campaigns for app: %w", err)
|
||||
}
|
||||
|
||||
return campaigns, nil
|
||||
}
|
||||
|
||||
func (s *campaignService) validateCampaignType(campaignType string) error {
|
||||
validTypes := []string{"REWARD", "POINTS", "TOKENS", "MIXED"}
|
||||
for _, validType := range validTypes {
|
||||
if campaignType == validType {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("invalid campaign type: %s. Valid types are: REWARD, POINTS, TOKENS, MIXED", campaignType)
|
||||
}
|
||||
|
||||
func (s *campaignService) validateDateRange(startDate, endDate time.Time) error {
|
||||
if startDate.IsZero() {
|
||||
return fmt.Errorf("start date is required")
|
||||
}
|
||||
if endDate.IsZero() {
|
||||
return fmt.Errorf("end date is required")
|
||||
}
|
||||
if startDate.After(endDate) {
|
||||
return fmt.Errorf("start date cannot be after end date")
|
||||
}
|
||||
if startDate.Before(time.Now()) {
|
||||
return fmt.Errorf("start date cannot be in the past")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *campaignService) validateCampaignRules(rules []contract.CampaignRuleStruct) error {
|
||||
if len(rules) == 0 {
|
||||
return fmt.Errorf("at least one rule is required")
|
||||
}
|
||||
|
||||
for i, rule := range rules {
|
||||
if err := s.validateRuleType(rule.RuleType); err != nil {
|
||||
return fmt.Errorf("invalid rule type in rule %d: %w", i+1, err)
|
||||
}
|
||||
|
||||
if err := s.validateRewardType(rule.RewardType); err != nil {
|
||||
return fmt.Errorf("invalid reward type in rule %d: %w", i+1, err)
|
||||
}
|
||||
|
||||
// Validate reward value based on reward type
|
||||
if rule.RewardType == "POINTS" || rule.RewardType == "TOKENS" {
|
||||
if rule.RewardValue == nil || *rule.RewardValue <= 0 {
|
||||
return fmt.Errorf("reward value must be positive for %s type in rule %d", rule.RewardType, i+1)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate reward reference ID for REWARD type
|
||||
if rule.RewardType == "REWARD" {
|
||||
if rule.RewardRefID == nil {
|
||||
return fmt.Errorf("reward reference ID is required for REWARD type in rule %d", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
_ = i // Avoid unused variable warning
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *campaignService) validateRuleType(ruleType string) error {
|
||||
validTypes := []string{"TIER", "SPEND", "PRODUCT", "CATEGORY", "DAY", "LOCATION"}
|
||||
for _, validType := range validTypes {
|
||||
if ruleType == validType {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("invalid rule type: %s. Valid types are: TIER, SPEND, PRODUCT, CATEGORY, DAY, LOCATION", ruleType)
|
||||
}
|
||||
|
||||
func (s *campaignService) validateRewardType(rewardType string) error {
|
||||
validTypes := []string{"POINTS", "TOKENS", "REWARD"}
|
||||
for _, validType := range validTypes {
|
||||
if rewardType == validType {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("invalid reward type: %s. Valid types are: POINTS, TOKENS, REWARD", rewardType)
|
||||
}
|
||||
198
internal/service/reward_service.go
Normal file
198
internal/service/reward_service.go
Normal file
@ -0,0 +1,198 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/processor"
|
||||
)
|
||||
|
||||
type RewardService interface {
|
||||
CreateReward(ctx context.Context, req *contract.CreateRewardRequest) (*models.RewardResponse, error)
|
||||
GetReward(ctx context.Context, id string) (*models.RewardResponse, error)
|
||||
ListRewards(ctx context.Context, req *contract.ListRewardsRequest) (*models.ListRewardsResponse, error)
|
||||
UpdateReward(ctx context.Context, req *contract.UpdateRewardRequest) (*models.RewardResponse, error)
|
||||
DeleteReward(ctx context.Context, id string) error
|
||||
UpdateRewardStock(ctx context.Context, id string, newStock int) error
|
||||
GetRewardsByType(ctx context.Context, rewardType string) ([]models.RewardResponse, error)
|
||||
}
|
||||
|
||||
type rewardService struct {
|
||||
rewardProcessor processor.RewardProcessor
|
||||
}
|
||||
|
||||
func NewRewardService(rewardProcessor processor.RewardProcessor) RewardService {
|
||||
return &rewardService{
|
||||
rewardProcessor: rewardProcessor,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *rewardService) CreateReward(ctx context.Context, req *contract.CreateRewardRequest) (*models.RewardResponse, error) {
|
||||
// Validate reward type
|
||||
if err := s.validateRewardType(req.RewardType); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Validate cost points
|
||||
if req.CostPoints <= 0 {
|
||||
return nil, fmt.Errorf("cost points must be greater than 0")
|
||||
}
|
||||
|
||||
// Validate stock for physical rewards
|
||||
if req.RewardType == "PHYSICAL" && req.Stock != nil && *req.Stock <= 0 {
|
||||
return nil, fmt.Errorf("physical rewards must have positive stock or unlimited stock")
|
||||
}
|
||||
|
||||
// Validate max per customer
|
||||
if req.MaxPerCustomer <= 0 {
|
||||
return nil, fmt.Errorf("max per customer must be greater than 0")
|
||||
}
|
||||
|
||||
// Create reward
|
||||
response, err := s.rewardProcessor.CreateReward(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create reward: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *rewardService) GetReward(ctx context.Context, id string) (*models.RewardResponse, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("reward ID is required")
|
||||
}
|
||||
|
||||
response, err := s.rewardProcessor.GetReward(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get reward: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *rewardService) ListRewards(ctx context.Context, req *contract.ListRewardsRequest) (*models.ListRewardsResponse, error) {
|
||||
// Set default pagination
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.Limit <= 0 {
|
||||
req.Limit = 10
|
||||
}
|
||||
if req.Limit > 100 {
|
||||
req.Limit = 100
|
||||
}
|
||||
|
||||
// Validate reward type filter if provided
|
||||
if req.RewardType != "" {
|
||||
if err := s.validateRewardType(req.RewardType); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate points range if provided
|
||||
if req.MinPoints != nil && req.MaxPoints != nil {
|
||||
if *req.MinPoints > *req.MaxPoints {
|
||||
return nil, fmt.Errorf("min points cannot be greater than max points")
|
||||
}
|
||||
}
|
||||
|
||||
response, err := s.rewardProcessor.ListRewards(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list rewards: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *rewardService) UpdateReward(ctx context.Context, req *contract.UpdateRewardRequest) (*models.RewardResponse, error) {
|
||||
if req.ID.String() == "" {
|
||||
return nil, fmt.Errorf("reward ID is required")
|
||||
}
|
||||
|
||||
// Validate reward type
|
||||
if err := s.validateRewardType(req.RewardType); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Validate cost points
|
||||
if req.CostPoints <= 0 {
|
||||
return nil, fmt.Errorf("cost points must be greater than 0")
|
||||
}
|
||||
|
||||
// Validate stock for physical rewards
|
||||
if req.RewardType == "PHYSICAL" && req.Stock != nil && *req.Stock <= 0 {
|
||||
return nil, fmt.Errorf("physical rewards must have positive stock or unlimited stock")
|
||||
}
|
||||
|
||||
// Validate max per customer
|
||||
if req.MaxPerCustomer <= 0 {
|
||||
return nil, fmt.Errorf("max per customer must be greater than 0")
|
||||
}
|
||||
|
||||
response, err := s.rewardProcessor.UpdateReward(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update reward: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *rewardService) DeleteReward(ctx context.Context, id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("reward ID is required")
|
||||
}
|
||||
|
||||
err := s.rewardProcessor.DeleteReward(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete reward: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *rewardService) UpdateRewardStock(ctx context.Context, id string, newStock int) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("reward ID is required")
|
||||
}
|
||||
|
||||
if newStock < 0 {
|
||||
return fmt.Errorf("stock cannot be negative")
|
||||
}
|
||||
|
||||
err := s.rewardProcessor.UpdateRewardStock(ctx, id, newStock)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update reward stock: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *rewardService) GetRewardsByType(ctx context.Context, rewardType string) ([]models.RewardResponse, error) {
|
||||
if rewardType == "" {
|
||||
return nil, fmt.Errorf("reward type is required")
|
||||
}
|
||||
|
||||
// Validate reward type
|
||||
if err := s.validateRewardType(rewardType); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
rewards, err := s.rewardProcessor.GetRewardsByType(ctx, rewardType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rewards by type: %w", err)
|
||||
}
|
||||
|
||||
return rewards, nil
|
||||
}
|
||||
|
||||
func (s *rewardService) validateRewardType(rewardType string) error {
|
||||
validTypes := []string{"VOUCHER", "PHYSICAL", "DIGITAL", "BALANCE"}
|
||||
for _, validType := range validTypes {
|
||||
if rewardType == validType {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("invalid reward type: %s. Valid types are: VOUCHER, PHYSICAL, DIGITAL, BALANCE", rewardType)
|
||||
}
|
||||
336
internal/validator/campaign_validator.go
Normal file
336
internal/validator/campaign_validator.go
Normal file
@ -0,0 +1,336 @@
|
||||
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
|
||||
}
|
||||
263
internal/validator/reward_validator.go
Normal file
263
internal/validator/reward_validator.go
Normal file
@ -0,0 +1,263 @@
|
||||
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)
|
||||
}
|
||||
6
migrations/000055_create_rewards_table.down.sql
Normal file
6
migrations/000055_create_rewards_table.down.sql
Normal file
@ -0,0 +1,6 @@
|
||||
-- Drop rewards table and its indexes
|
||||
DROP INDEX IF EXISTS idx_rewards_stock;
|
||||
DROP INDEX IF EXISTS idx_rewards_created_at;
|
||||
DROP INDEX IF EXISTS idx_rewards_cost_points;
|
||||
DROP INDEX IF EXISTS idx_rewards_reward_type;
|
||||
DROP TABLE IF EXISTS rewards;
|
||||
30
migrations/000055_create_rewards_table.up.sql
Normal file
30
migrations/000055_create_rewards_table.up.sql
Normal file
@ -0,0 +1,30 @@
|
||||
-- Create rewards table
|
||||
CREATE TABLE rewards (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(150) NOT NULL,
|
||||
reward_type VARCHAR(50) NOT NULL, -- VOUCHER, PHYSICAL, DIGITAL, BALANCE
|
||||
cost_points BIGINT NOT NULL,
|
||||
stock INT, -- nullable if unlimited (digital rewards)
|
||||
max_per_customer INT DEFAULT 1,
|
||||
tnc JSONB, -- sections + rules + expiry_days
|
||||
metadata JSONB, -- brand, model, etc.
|
||||
images JSONB, -- array of images
|
||||
created_at TIMESTAMP DEFAULT now(),
|
||||
updated_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_rewards_reward_type ON rewards(reward_type);
|
||||
CREATE INDEX idx_rewards_cost_points ON rewards(cost_points);
|
||||
CREATE INDEX idx_rewards_created_at ON rewards(created_at);
|
||||
CREATE INDEX idx_rewards_stock ON rewards(stock) WHERE stock IS NOT NULL;
|
||||
|
||||
-- Add comments
|
||||
COMMENT ON TABLE rewards IS 'Rewards catalog for gamification system';
|
||||
COMMENT ON COLUMN rewards.reward_type IS 'Type of reward: VOUCHER, PHYSICAL, DIGITAL, BALANCE';
|
||||
COMMENT ON COLUMN rewards.cost_points IS 'Points required to redeem this reward';
|
||||
COMMENT ON COLUMN rewards.stock IS 'Available quantity (NULL for unlimited digital rewards)';
|
||||
COMMENT ON COLUMN rewards.max_per_customer IS 'Maximum times a customer can redeem this reward';
|
||||
COMMENT ON COLUMN rewards.tnc IS 'Terms and conditions in JSON format';
|
||||
COMMENT ON COLUMN rewards.metadata IS 'Additional reward metadata (brand, model, etc.)';
|
||||
COMMENT ON COLUMN rewards.images IS 'Array of image URLs and types';
|
||||
15
migrations/000056_create_campaigns_tables.down.sql
Normal file
15
migrations/000056_create_campaigns_tables.down.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- Drop campaign_rules table and its indexes first (due to foreign key constraint)
|
||||
DROP INDEX IF EXISTS idx_campaign_rules_reward_ref_id;
|
||||
DROP INDEX IF EXISTS idx_campaign_rules_reward_type;
|
||||
DROP INDEX IF EXISTS idx_campaign_rules_rule_type;
|
||||
DROP INDEX IF EXISTS idx_campaign_rules_campaign_id;
|
||||
DROP TABLE IF EXISTS campaign_rules;
|
||||
|
||||
-- Drop campaigns table and its indexes
|
||||
DROP INDEX IF EXISTS idx_campaigns_position;
|
||||
DROP INDEX IF EXISTS idx_campaigns_show_on_app;
|
||||
DROP INDEX IF EXISTS idx_campaigns_is_active;
|
||||
DROP INDEX IF EXISTS idx_campaigns_end_date;
|
||||
DROP INDEX IF EXISTS idx_campaigns_start_date;
|
||||
DROP INDEX IF EXISTS idx_campaigns_type;
|
||||
DROP TABLE IF EXISTS campaigns;
|
||||
64
migrations/000056_create_campaigns_tables.up.sql
Normal file
64
migrations/000056_create_campaigns_tables.up.sql
Normal file
@ -0,0 +1,64 @@
|
||||
-- Create campaigns table
|
||||
CREATE TABLE campaigns (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(150) NOT NULL,
|
||||
description TEXT,
|
||||
type VARCHAR(50) NOT NULL, -- REWARD, POINTS, TOKENS, MIXED
|
||||
start_date TIMESTAMP NOT NULL,
|
||||
end_date TIMESTAMP NOT NULL,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
show_on_app BOOLEAN DEFAULT true, -- apakah campaign muncul di aplikasi
|
||||
position INT DEFAULT 0, -- urutan tampil di aplikasi
|
||||
metadata JSONB, -- fleksibel: banner, theme, config lain
|
||||
created_at TIMESTAMP DEFAULT now(),
|
||||
updated_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
|
||||
-- Create campaign_rules table
|
||||
CREATE TABLE campaign_rules (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
campaign_id UUID NOT NULL REFERENCES campaigns(id) ON DELETE CASCADE,
|
||||
|
||||
-- Rule definition
|
||||
rule_type VARCHAR(50) NOT NULL, -- TIER, SPEND, PRODUCT, CATEGORY, DAY, LOCATION
|
||||
condition_value VARCHAR(255), -- e.g. SILVER, 50000, uuid-product, MONDAY
|
||||
|
||||
-- Reward definition
|
||||
reward_type VARCHAR(50) NOT NULL, -- POINTS, TOKENS, REWARD
|
||||
reward_value BIGINT, -- e.g. 10 (points/tokens), 2 (multiplier)
|
||||
reward_subtype VARCHAR(50), -- e.g. MULTIPLIER, SPIN, BONUS, PHYSICAL
|
||||
reward_ref_id UUID, -- FK ke rewards.id kalau reward_type = REWARD
|
||||
|
||||
metadata JSONB, -- fleksibel: extra configs
|
||||
|
||||
created_at TIMESTAMP DEFAULT now(),
|
||||
updated_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_campaigns_type ON campaigns(type);
|
||||
CREATE INDEX idx_campaigns_start_date ON campaigns(start_date);
|
||||
CREATE INDEX idx_campaigns_end_date ON campaigns(end_date);
|
||||
CREATE INDEX idx_campaigns_is_active ON campaigns(is_active);
|
||||
CREATE INDEX idx_campaigns_show_on_app ON campaigns(show_on_app);
|
||||
CREATE INDEX idx_campaigns_position ON campaigns(position);
|
||||
|
||||
CREATE INDEX idx_campaign_rules_campaign_id ON campaign_rules(campaign_id);
|
||||
CREATE INDEX idx_campaign_rules_rule_type ON campaign_rules(rule_type);
|
||||
CREATE INDEX idx_campaign_rules_reward_type ON campaign_rules(reward_type);
|
||||
CREATE INDEX idx_campaign_rules_reward_ref_id ON campaign_rules(reward_ref_id);
|
||||
|
||||
-- Add comments
|
||||
COMMENT ON TABLE campaigns IS 'Campaign definitions for gamification system';
|
||||
COMMENT ON TABLE campaign_rules IS 'Rules and rewards for each campaign';
|
||||
|
||||
COMMENT ON COLUMN campaigns.type IS 'Type of campaign: REWARD, POINTS, TOKENS, MIXED';
|
||||
COMMENT ON COLUMN campaigns.show_on_app IS 'Whether campaign appears in mobile app';
|
||||
COMMENT ON COLUMN campaigns.position IS 'Display order in app (lower number = higher priority)';
|
||||
|
||||
COMMENT ON COLUMN campaign_rules.rule_type IS 'Rule condition type: TIER, SPEND, PRODUCT, CATEGORY, DAY, LOCATION';
|
||||
COMMENT ON COLUMN campaign_rules.condition_value IS 'Value for the rule condition (tier name, amount, product ID, etc.)';
|
||||
COMMENT ON COLUMN campaign_rules.reward_type IS 'Type of reward: POINTS, TOKENS, REWARD';
|
||||
COMMENT ON COLUMN campaign_rules.reward_value IS 'Reward amount or multiplier';
|
||||
COMMENT ON COLUMN campaign_rules.reward_subtype IS 'Subtype of reward: MULTIPLIER, SPIN, BONUS, PHYSICAL';
|
||||
COMMENT ON COLUMN campaign_rules.reward_ref_id IS 'Reference to rewards table when reward_type = REWARD';
|
||||
Loading…
x
Reference in New Issue
Block a user