diff --git a/internal/app/app.go b/internal/app/app.go index 6b224ab..0485697 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -94,6 +94,8 @@ func (a *App) Initialize(cfg *config.Config) error { validators.accountValidator, *services.orderIngredientTransactionService, validators.orderIngredientTransactionValidator, + services.gamificationService, + validators.gamificationValidator, ) return nil @@ -167,6 +169,13 @@ type repositories struct { chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl accountRepo *repository.AccountRepositoryImpl orderIngredientTransactionRepo *repository.OrderIngredientTransactionRepositoryImpl + customerPointsRepo *repository.CustomerPointsRepository + customerTokensRepo *repository.CustomerTokensRepository + tierRepo *repository.TierRepository + gameRepo *repository.GameRepository + gamePrizeRepo *repository.GamePrizeRepository + gamePlayRepo *repository.GamePlayRepository + omsetTrackerRepo *repository.OmsetTrackerRepository txManager *repository.TxManager } @@ -200,7 +209,14 @@ func (a *App) initRepositories() *repositories { chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db), accountRepo: repository.NewAccountRepositoryImpl(a.db), orderIngredientTransactionRepo: repository.NewOrderIngredientTransactionRepositoryImpl(a.db).(*repository.OrderIngredientTransactionRepositoryImpl), - txManager: repository.NewTxManager(a.db), + customerPointsRepo: repository.NewCustomerPointsRepository(a.db), + customerTokensRepo: repository.NewCustomerTokensRepository(a.db), + tierRepo: repository.NewTierRepository(a.db), + gameRepo: repository.NewGameRepository(a.db), + gamePrizeRepo: repository.NewGamePrizeRepository(a.db), + gamePlayRepo: repository.NewGamePlayRepository(a.db), + omsetTrackerRepo: repository.NewOmsetTrackerRepository(a.db), + txManager: repository.NewTxManager(a.db), } } @@ -229,6 +245,13 @@ type processors struct { chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl accountProcessor *processor.AccountProcessorImpl orderIngredientTransactionProcessor *processor.OrderIngredientTransactionProcessorImpl + customerPointsProcessor *processor.CustomerPointsProcessor + customerTokensProcessor *processor.CustomerTokensProcessor + tierProcessor *processor.TierProcessor + gameProcessor *processor.GameProcessor + gamePrizeProcessor *processor.GamePrizeProcessor + gamePlayProcessor *processor.GamePlayProcessor + omsetTrackerProcessor *processor.OmsetTrackerProcessor fileClient processor.FileClient inventoryMovementService service.InventoryMovementService } @@ -262,6 +285,13 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo), accountProcessor: processor.NewAccountProcessorImpl(repos.accountRepo, repos.chartOfAccountRepo), orderIngredientTransactionProcessor: processor.NewOrderIngredientTransactionProcessorImpl(repos.orderIngredientTransactionRepo, repos.productRecipeRepo, repos.ingredientRepo, repos.unitRepo).(*processor.OrderIngredientTransactionProcessorImpl), + customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo), + customerTokensProcessor: processor.NewCustomerTokensProcessor(repos.customerTokensRepo), + tierProcessor: processor.NewTierProcessor(repos.tierRepo), + gameProcessor: processor.NewGameProcessor(repos.gameRepo), + gamePrizeProcessor: processor.NewGamePrizeProcessor(repos.gamePrizeRepo), + gamePlayProcessor: processor.NewGamePlayProcessor(repos.gamePlayRepo, repos.gameRepo, repos.gamePrizeRepo, repos.customerTokensRepo, repos.customerPointsRepo), + omsetTrackerProcessor: processor.NewOmsetTrackerProcessor(repos.omsetTrackerRepo), fileClient: fileClient, inventoryMovementService: inventoryMovementService, } @@ -294,6 +324,7 @@ type services struct { chartOfAccountService service.ChartOfAccountService accountService service.AccountService orderIngredientTransactionService *service.OrderIngredientTransactionService + gamificationService service.GamificationService } func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services { @@ -324,6 +355,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor) 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) // Update order service with order ingredient transaction service orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager) @@ -355,6 +387,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con chartOfAccountService: chartOfAccountService, accountService: accountService, orderIngredientTransactionService: orderIngredientTransactionService, + gamificationService: gamificationService, } } @@ -388,6 +421,7 @@ type validators struct { chartOfAccountValidator *validator.ChartOfAccountValidatorImpl accountValidator *validator.AccountValidatorImpl orderIngredientTransactionValidator *validator.OrderIngredientTransactionValidatorImpl + gamificationValidator *validator.GamificationValidatorImpl } func (a *App) initValidators() *validators { @@ -411,5 +445,6 @@ func (a *App) initValidators() *validators { chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl), accountValidator: validator.NewAccountValidator().(*validator.AccountValidatorImpl), orderIngredientTransactionValidator: validator.NewOrderIngredientTransactionValidator().(*validator.OrderIngredientTransactionValidatorImpl), + gamificationValidator: validator.NewGamificationValidator(), } } diff --git a/internal/constants/error.go b/internal/constants/error.go index 17d86ee..33aafd6 100644 --- a/internal/constants/error.go +++ b/internal/constants/error.go @@ -42,6 +42,14 @@ const ( PurchaseOrderServiceEntity = "purchase_order_service" IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service" TableEntity = "table" + // Gamification entities + CustomerPointsEntity = "customer_points" + CustomerTokensEntity = "customer_tokens" + TierEntity = "tier" + GameEntity = "game" + GamePrizeEntity = "game_prize" + GamePlayEntity = "game_play" + OmsetTrackerEntity = "omset_tracker" ) var HttpErrorMap = map[string]int{ diff --git a/internal/contract/customer_points_contract.go b/internal/contract/customer_points_contract.go new file mode 100644 index 0000000..04bdded --- /dev/null +++ b/internal/contract/customer_points_contract.go @@ -0,0 +1,49 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateCustomerPointsRequest struct { + CustomerID uuid.UUID `json:"customer_id" validate:"required"` + Balance int64 `json:"balance" validate:"min=0"` +} + +type UpdateCustomerPointsRequest struct { + Balance int64 `json:"balance" validate:"min=0"` +} + +type AddCustomerPointsRequest struct { + Points int64 `json:"points" validate:"required,min=1"` +} + +type DeductCustomerPointsRequest struct { + Points int64 `json:"points" validate:"required,min=1"` +} + +type CustomerPointsResponse struct { + ID uuid.UUID `json:"id"` + CustomerID uuid.UUID `json:"customer_id"` + Balance int64 `json:"balance"` + Customer *CustomerResponse `json:"customer,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListCustomerPointsRequest struct { + Page int `json:"page" validate:"min=1"` + Limit int `json:"limit" validate:"min=1,max=100"` + Search string `json:"search"` + SortBy string `json:"sort_by" validate:"omitempty,oneof=balance created_at updated_at"` + SortOrder string `json:"sort_order" validate:"omitempty,oneof=asc desc"` +} + +type PaginatedCustomerPointsResponse struct { + Data []CustomerPointsResponse `json:"data"` + TotalCount int `json:"total_count"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} diff --git a/internal/contract/customer_tokens_contract.go b/internal/contract/customer_tokens_contract.go new file mode 100644 index 0000000..21d90c6 --- /dev/null +++ b/internal/contract/customer_tokens_contract.go @@ -0,0 +1,52 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateCustomerTokensRequest struct { + CustomerID uuid.UUID `json:"customer_id" validate:"required"` + TokenType string `json:"token_type" validate:"required,oneof=SPIN RAFFLE MINIGAME"` + Balance int64 `json:"balance" validate:"min=0"` +} + +type UpdateCustomerTokensRequest struct { + Balance int64 `json:"balance" validate:"min=0"` +} + +type AddCustomerTokensRequest struct { + Tokens int64 `json:"tokens" validate:"required,min=1"` +} + +type DeductCustomerTokensRequest struct { + Tokens int64 `json:"tokens" validate:"required,min=1"` +} + +type CustomerTokensResponse struct { + ID uuid.UUID `json:"id"` + CustomerID uuid.UUID `json:"customer_id"` + TokenType string `json:"token_type"` + Balance int64 `json:"balance"` + Customer *CustomerResponse `json:"customer,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListCustomerTokensRequest struct { + Page int `json:"page" validate:"min=1"` + Limit int `json:"limit" validate:"min=1,max=100"` + Search string `json:"search"` + TokenType string `json:"token_type" validate:"omitempty,oneof=SPIN RAFFLE MINIGAME"` + SortBy string `json:"sort_by" validate:"omitempty,oneof=balance token_type created_at updated_at"` + SortOrder string `json:"sort_order" validate:"omitempty,oneof=asc desc"` +} + +type PaginatedCustomerTokensResponse struct { + Data []CustomerTokensResponse `json:"data"` + TotalCount int `json:"total_count"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} diff --git a/internal/contract/game_contract.go b/internal/contract/game_contract.go new file mode 100644 index 0000000..754acf8 --- /dev/null +++ b/internal/contract/game_contract.go @@ -0,0 +1,49 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateGameRequest struct { + Name string `json:"name" validate:"required"` + Type string `json:"type" validate:"required,oneof=SPIN RAFFLE MINIGAME"` + IsActive bool `json:"is_active"` + Metadata map[string]interface{} `json:"metadata"` +} + +type UpdateGameRequest struct { + Name *string `json:"name,omitempty" validate:"omitempty,required"` + Type *string `json:"type,omitempty" validate:"omitempty,oneof=SPIN RAFFLE MINIGAME"` + IsActive *bool `json:"is_active,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type GameResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsActive bool `json:"is_active"` + Metadata map[string]interface{} `json:"metadata"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListGamesRequest struct { + Page int `json:"page" form:"page" validate:"min=1"` + Limit int `json:"limit" form:"limit" validate:"min=1,max=100"` + Search string `json:"search" form:"search"` + Type string `json:"type" form:"type" validate:"omitempty,oneof=SPIN RAFFLE MINIGAME"` + IsActive *bool `json:"is_active" form:"is_active"` + SortBy string `json:"sort_by" form:"sort_by" validate:"omitempty,oneof=name type created_at updated_at"` + SortOrder string `json:"sort_order" form:"sort_order" validate:"omitempty,oneof=asc desc"` +} + +type PaginatedGamesResponse struct { + Data []GameResponse `json:"data"` + TotalCount int `json:"total_count"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} diff --git a/internal/contract/game_play_contract.go b/internal/contract/game_play_contract.go new file mode 100644 index 0000000..57ab50d --- /dev/null +++ b/internal/contract/game_play_contract.go @@ -0,0 +1,58 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateGamePlayRequest struct { + GameID uuid.UUID `json:"game_id" validate:"required"` + CustomerID uuid.UUID `json:"customer_id" validate:"required"` + TokenUsed int `json:"token_used" validate:"min=0"` + RandomSeed *string `json:"random_seed,omitempty"` +} + +type GamePlayResponse struct { + ID uuid.UUID `json:"id"` + GameID uuid.UUID `json:"game_id"` + CustomerID uuid.UUID `json:"customer_id"` + PrizeID *uuid.UUID `json:"prize_id,omitempty"` + TokenUsed int `json:"token_used"` + RandomSeed *string `json:"random_seed,omitempty"` + CreatedAt time.Time `json:"created_at"` + Game *GameResponse `json:"game,omitempty"` + Customer *CustomerResponse `json:"customer,omitempty"` + Prize *GamePrizeResponse `json:"prize,omitempty"` +} + +type ListGamePlaysRequest struct { + Page int `json:"page" validate:"min=1"` + Limit int `json:"limit" validate:"min=1,max=100"` + Search string `json:"search"` + GameID *uuid.UUID `json:"game_id"` + CustomerID *uuid.UUID `json:"customer_id"` + PrizeID *uuid.UUID `json:"prize_id"` + SortBy string `json:"sort_by" validate:"omitempty,oneof=created_at token_used"` + SortOrder string `json:"sort_order" validate:"omitempty,oneof=asc desc"` +} + +type PaginatedGamePlaysResponse struct { + Data []GamePlayResponse `json:"data"` + TotalCount int `json:"total_count"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} + +type PlayGameRequest struct { + GameID uuid.UUID `json:"game_id" validate:"required"` + CustomerID uuid.UUID `json:"customer_id" validate:"required"` + TokenUsed int `json:"token_used" validate:"min=0"` +} + +type PlayGameResponse struct { + GamePlay GamePlayResponse `json:"game_play"` + PrizeWon *GamePrizeResponse `json:"prize_won,omitempty"` + TokensRemaining int64 `json:"tokens_remaining"` +} diff --git a/internal/contract/game_prize_contract.go b/internal/contract/game_prize_contract.go new file mode 100644 index 0000000..69c9ae1 --- /dev/null +++ b/internal/contract/game_prize_contract.go @@ -0,0 +1,61 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateGamePrizeRequest struct { + GameID uuid.UUID `json:"game_id" validate:"required"` + Name string `json:"name" validate:"required"` + Weight int `json:"weight" validate:"min=1"` + Stock int `json:"stock" validate:"min=0"` + MaxStock *int `json:"max_stock,omitempty"` + Threshold *int64 `json:"threshold,omitempty"` + FallbackPrizeID *uuid.UUID `json:"fallback_prize_id,omitempty"` + Metadata map[string]interface{} `json:"metadata"` +} + +type UpdateGamePrizeRequest struct { + Name *string `json:"name,omitempty" validate:"omitempty,required"` + Weight *int `json:"weight,omitempty" validate:"omitempty,min=1"` + Stock *int `json:"stock,omitempty" validate:"omitempty,min=0"` + MaxStock *int `json:"max_stock,omitempty"` + Threshold *int64 `json:"threshold,omitempty"` + FallbackPrizeID *uuid.UUID `json:"fallback_prize_id,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type GamePrizeResponse struct { + ID uuid.UUID `json:"id"` + GameID uuid.UUID `json:"game_id"` + Name string `json:"name"` + Weight int `json:"weight"` + Stock int `json:"stock"` + MaxStock *int `json:"max_stock,omitempty"` + Threshold *int64 `json:"threshold,omitempty"` + FallbackPrizeID *uuid.UUID `json:"fallback_prize_id,omitempty"` + Metadata map[string]interface{} `json:"metadata"` + Game *GameResponse `json:"game,omitempty"` + FallbackPrize *GamePrizeResponse `json:"fallback_prize,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListGamePrizesRequest struct { + Page int `json:"page" form:"page" validate:"min=1"` + Limit int `json:"limit" form:"limit" validate:"min=1,max=100"` + Search string `json:"search" form:"search"` + GameID *uuid.UUID `json:"game_id" form:"game_id"` + SortBy string `json:"sort_by" form:"sort_by" validate:"omitempty,oneof=name weight stock created_at updated_at"` + SortOrder string `json:"sort_order" form:"sort_order" validate:"omitempty,oneof=asc desc"` +} + +type PaginatedGamePrizesResponse struct { + Data []GamePrizeResponse `json:"data"` + TotalCount int `json:"total_count"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} diff --git a/internal/contract/omset_tracker_contract.go b/internal/contract/omset_tracker_contract.go new file mode 100644 index 0000000..0b14243 --- /dev/null +++ b/internal/contract/omset_tracker_contract.go @@ -0,0 +1,59 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateOmsetTrackerRequest struct { + PeriodType string `json:"period_type" validate:"required,oneof=DAILY WEEKLY MONTHLY TOTAL"` + PeriodStart time.Time `json:"period_start" validate:"required"` + PeriodEnd time.Time `json:"period_end" validate:"required"` + Total int64 `json:"total" validate:"min=0"` + GameID *uuid.UUID `json:"game_id,omitempty"` +} + +type UpdateOmsetTrackerRequest struct { + PeriodType *string `json:"period_type,omitempty" validate:"omitempty,oneof=DAILY WEEKLY MONTHLY TOTAL"` + PeriodStart *time.Time `json:"period_start,omitempty"` + PeriodEnd *time.Time `json:"period_end,omitempty"` + Total *int64 `json:"total,omitempty" validate:"omitempty,min=0"` + GameID *uuid.UUID `json:"game_id,omitempty"` +} + +type AddOmsetRequest struct { + Amount int64 `json:"amount" validate:"required,min=1"` +} + +type OmsetTrackerResponse struct { + ID uuid.UUID `json:"id"` + PeriodType string `json:"period_type"` + PeriodStart time.Time `json:"period_start"` + PeriodEnd time.Time `json:"period_end"` + Total int64 `json:"total"` + GameID *uuid.UUID `json:"game_id,omitempty"` + Game *GameResponse `json:"game,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListOmsetTrackerRequest struct { + Page int `json:"page" validate:"min=1"` + Limit int `json:"limit" validate:"min=1,max=100"` + Search string `json:"search"` + PeriodType string `json:"period_type" validate:"omitempty,oneof=DAILY WEEKLY MONTHLY TOTAL"` + GameID *uuid.UUID `json:"game_id"` + From *time.Time `json:"from"` + To *time.Time `json:"to"` + SortBy string `json:"sort_by" validate:"omitempty,oneof=period_type period_start total created_at updated_at"` + SortOrder string `json:"sort_order" validate:"omitempty,oneof=asc desc"` +} + +type PaginatedOmsetTrackerResponse struct { + Data []OmsetTrackerResponse `json:"data"` + TotalCount int `json:"total_count"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} diff --git a/internal/contract/tier_contract.go b/internal/contract/tier_contract.go new file mode 100644 index 0000000..7493a7d --- /dev/null +++ b/internal/contract/tier_contract.go @@ -0,0 +1,44 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateTierRequest struct { + Name string `json:"name" validate:"required"` + MinPoints int64 `json:"min_points" validate:"min=0"` + Benefits map[string]interface{} `json:"benefits"` +} + +type UpdateTierRequest struct { + Name *string `json:"name,omitempty" validate:"omitempty,required"` + MinPoints *int64 `json:"min_points,omitempty" validate:"omitempty,min=0"` + Benefits map[string]interface{} `json:"benefits,omitempty"` +} + +type TierResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + MinPoints int64 `json:"min_points"` + Benefits map[string]interface{} `json:"benefits"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListTiersRequest struct { + Page int `form:"page" validate:"min=1"` + Limit int `form:"limit" validate:"min=1,max=100"` + Search string `form:"search"` + SortBy string `form:"sort_by" validate:"omitempty,oneof=name min_points created_at updated_at"` + SortOrder string `form:"sort_order" validate:"omitempty,oneof=asc desc"` +} + +type PaginatedTiersResponse struct { + Data []TierResponse `json:"data"` + TotalCount int `json:"total_count"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} diff --git a/internal/entities/customer_points.go b/internal/entities/customer_points.go new file mode 100644 index 0000000..7de4873 --- /dev/null +++ b/internal/entities/customer_points.go @@ -0,0 +1,29 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type CustomerPoints struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + CustomerID uuid.UUID `gorm:"type:uuid;not null;index" json:"customer_id" validate:"required"` + Balance int64 `gorm:"not null;default:0" json:"balance" validate:"min=0"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Customer Customer `gorm:"foreignKey:CustomerID" json:"customer,omitempty"` +} + +func (cp *CustomerPoints) BeforeCreate(tx *gorm.DB) error { + if cp.ID == uuid.Nil { + cp.ID = uuid.New() + } + return nil +} + +func (CustomerPoints) TableName() string { + return "customer_points" +} diff --git a/internal/entities/customer_tokens.go b/internal/entities/customer_tokens.go new file mode 100644 index 0000000..eb62593 --- /dev/null +++ b/internal/entities/customer_tokens.go @@ -0,0 +1,38 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type TokenType string + +const ( + TokenTypeSpin TokenType = "SPIN" + TokenTypeRaffle TokenType = "RAFFLE" + TokenTypeMinigame TokenType = "MINIGAME" +) + +type CustomerTokens struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + CustomerID uuid.UUID `gorm:"type:uuid;not null;index" json:"customer_id" validate:"required"` + TokenType TokenType `gorm:"type:varchar(50);not null" json:"token_type" validate:"required,oneof=SPIN RAFFLE MINIGAME"` + Balance int64 `gorm:"not null;default:0" json:"balance" validate:"min=0"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Customer Customer `gorm:"foreignKey:CustomerID" json:"customer,omitempty"` +} + +func (ct *CustomerTokens) BeforeCreate(tx *gorm.DB) error { + if ct.ID == uuid.Nil { + ct.ID = uuid.New() + } + return nil +} + +func (CustomerTokens) TableName() string { + return "customer_tokens" +} diff --git a/internal/entities/entities.go b/internal/entities/entities.go index 1805974..d46008f 100644 --- a/internal/entities/entities.go +++ b/internal/entities/entities.go @@ -23,6 +23,14 @@ func GetAllEntities() []interface{} { &PurchaseOrderItem{}, &PurchaseOrderAttachment{}, &IngredientUnitConverter{}, + // Gamification entities + &CustomerPoints{}, + &CustomerTokens{}, + &Tier{}, + &Game{}, + &GamePrize{}, + &GamePlay{}, + &OmsetTracker{}, // Analytics entities are not database tables, they are query results } } diff --git a/internal/entities/game.go b/internal/entities/game.go new file mode 100644 index 0000000..a230f72 --- /dev/null +++ b/internal/entities/game.go @@ -0,0 +1,40 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type GameType string + +const ( + GameTypeSpin GameType = "SPIN" + GameTypeRaffle GameType = "RAFFLE" + GameTypeMinigame GameType = "MINIGAME" +) + +type Game struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + Name string `gorm:"type:varchar(255);not null" json:"name" validate:"required"` + Type GameType `gorm:"type:varchar(50);not null" json:"type" validate:"required,oneof=SPIN RAFFLE MINIGAME"` + IsActive bool `gorm:"default:true" json:"is_active"` + Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Prizes []GamePrize `gorm:"foreignKey:GameID" json:"prizes,omitempty"` + Plays []GamePlay `gorm:"foreignKey:GameID" json:"plays,omitempty"` +} + +func (g *Game) BeforeCreate(tx *gorm.DB) error { + if g.ID == uuid.Nil { + g.ID = uuid.New() + } + return nil +} + +func (Game) TableName() string { + return "games" +} diff --git a/internal/entities/game_play.go b/internal/entities/game_play.go new file mode 100644 index 0000000..5ef61f6 --- /dev/null +++ b/internal/entities/game_play.go @@ -0,0 +1,33 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type GamePlay struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + GameID uuid.UUID `gorm:"type:uuid;not null;index" json:"game_id" validate:"required"` + CustomerID uuid.UUID `gorm:"type:uuid;not null;index" json:"customer_id" validate:"required"` + PrizeID *uuid.UUID `gorm:"type:uuid" json:"prize_id,omitempty"` + TokenUsed int `gorm:"default:0" json:"token_used" validate:"min=0"` + RandomSeed *string `gorm:"type:varchar(255)" json:"random_seed,omitempty"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + + Game Game `gorm:"foreignKey:GameID" json:"game,omitempty"` + Customer Customer `gorm:"foreignKey:CustomerID" json:"customer,omitempty"` + Prize *GamePrize `gorm:"foreignKey:PrizeID" json:"prize,omitempty"` +} + +func (gp *GamePlay) BeforeCreate(tx *gorm.DB) error { + if gp.ID == uuid.Nil { + gp.ID = uuid.New() + } + return nil +} + +func (GamePlay) TableName() string { + return "game_plays" +} diff --git a/internal/entities/game_prize.go b/internal/entities/game_prize.go new file mode 100644 index 0000000..b277c6b --- /dev/null +++ b/internal/entities/game_prize.go @@ -0,0 +1,37 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type GamePrize struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + GameID uuid.UUID `gorm:"type:uuid;not null;index" json:"game_id" validate:"required"` + Name string `gorm:"type:varchar(255);not null" json:"name" validate:"required"` + Weight int `gorm:"not null" json:"weight" validate:"min=1"` + Stock int `gorm:"default:0" json:"stock" validate:"min=0"` + MaxStock *int `gorm:"" json:"max_stock,omitempty"` + Threshold *int64 `gorm:"" json:"threshold,omitempty"` + FallbackPrizeID *uuid.UUID `gorm:"type:uuid" json:"fallback_prize_id,omitempty"` + Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Game Game `gorm:"foreignKey:GameID" json:"game,omitempty"` + FallbackPrize *GamePrize `gorm:"foreignKey:FallbackPrizeID" json:"fallback_prize,omitempty"` + Plays []GamePlay `gorm:"foreignKey:PrizeID" json:"plays,omitempty"` +} + +func (gp *GamePrize) BeforeCreate(tx *gorm.DB) error { + if gp.ID == uuid.Nil { + gp.ID = uuid.New() + } + return nil +} + +func (GamePrize) TableName() string { + return "game_prizes" +} diff --git a/internal/entities/omset_tracker.go b/internal/entities/omset_tracker.go new file mode 100644 index 0000000..a0843fc --- /dev/null +++ b/internal/entities/omset_tracker.go @@ -0,0 +1,41 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type PeriodType string + +const ( + PeriodTypeDaily PeriodType = "DAILY" + PeriodTypeWeekly PeriodType = "WEEKLY" + PeriodTypeMonthly PeriodType = "MONTHLY" + PeriodTypeTotal PeriodType = "TOTAL" +) + +type OmsetTracker struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + PeriodType PeriodType `gorm:"type:varchar(20);not null" json:"period_type" validate:"required,oneof=DAILY WEEKLY MONTHLY TOTAL"` + PeriodStart time.Time `gorm:"type:date;not null" json:"period_start" validate:"required"` + PeriodEnd time.Time `gorm:"type:date;not null" json:"period_end" validate:"required"` + Total int64 `gorm:"not null;default:0" json:"total" validate:"min=0"` + GameID *uuid.UUID `gorm:"type:uuid" json:"game_id,omitempty"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Game *Game `gorm:"foreignKey:GameID" json:"game,omitempty"` +} + +func (ot *OmsetTracker) BeforeCreate(tx *gorm.DB) error { + if ot.ID == uuid.Nil { + ot.ID = uuid.New() + } + return nil +} + +func (OmsetTracker) TableName() string { + return "omset_tracker" +} diff --git a/internal/entities/tier.go b/internal/entities/tier.go new file mode 100644 index 0000000..1b3014b --- /dev/null +++ b/internal/entities/tier.go @@ -0,0 +1,28 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Tier struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + Name string `gorm:"type:varchar(100);not null;unique" json:"name" validate:"required"` + MinPoints int64 `gorm:"not null" json:"min_points" validate:"min=0"` + Benefits Metadata `gorm:"type:jsonb;default:'{}'" json:"benefits"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` +} + +func (t *Tier) BeforeCreate(tx *gorm.DB) error { + if t.ID == uuid.Nil { + t.ID = uuid.New() + } + return nil +} + +func (Tier) TableName() string { + return "tiers" +} diff --git a/internal/handler/gamification_handler.go b/internal/handler/gamification_handler.go new file mode 100644 index 0000000..4344a35 --- /dev/null +++ b/internal/handler/gamification_handler.go @@ -0,0 +1,527 @@ +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" + "github.com/google/uuid" +) + +type GamificationHandler struct { + gamificationService service.GamificationService + gamificationValidator validator.GamificationValidator +} + +func NewGamificationHandler( + gamificationService service.GamificationService, + gamificationValidator validator.GamificationValidator, +) *GamificationHandler { + return &GamificationHandler{ + gamificationService: gamificationService, + gamificationValidator: gamificationValidator, + } +} + +// Customer Points Handlers +func (h *GamificationHandler) CreateCustomerPoints(c *gin.Context) { + ctx := c.Request.Context() + + var req contract.CreateCustomerPointsRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateCustomerPoints -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateCustomerPoints") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateCreateCustomerPointsRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::CreateCustomerPoints -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateCustomerPoints") + return + } + + response, err := h.gamificationService.CreateCustomerPoints(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateCustomerPoints -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::CreateCustomerPoints") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::CreateCustomerPoints") +} + +func (h *GamificationHandler) GetCustomerPoints(c *gin.Context) { + ctx := c.Request.Context() + + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerPoints -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerPointsEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetCustomerPoints") + return + } + + response, err := h.gamificationService.GetCustomerPoints(ctx, id) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerPoints -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::GetCustomerPoints") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetCustomerPoints") +} + +func (h *GamificationHandler) GetCustomerPointsByCustomerID(c *gin.Context) { + ctx := c.Request.Context() + customerIDStr := c.Param("customer_id") + customerID, err := uuid.Parse(customerIDStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerPointsByCustomerID -> invalid customer ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerPointsEntity, "Invalid customer ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetCustomerPointsByCustomerID") + return + } + + response, err := h.gamificationService.GetCustomerPointsByCustomerID(ctx, customerID) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerPointsByCustomerID -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::GetCustomerPointsByCustomerID") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetCustomerPointsByCustomerID") +} + +func (h *GamificationHandler) ListCustomerPoints(c *gin.Context) { + ctx := c.Request.Context() + var req contract.ListCustomerPointsRequest + if err := c.ShouldBindQuery(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListCustomerPoints -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListCustomerPoints") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateListCustomerPointsRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::ListCustomerPoints -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListCustomerPoints") + return + } + + response, err := h.gamificationService.ListCustomerPoints(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListCustomerPoints -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::ListCustomerPoints") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::ListCustomerPoints") +} + +func (h *GamificationHandler) UpdateCustomerPoints(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateCustomerPoints -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerPointsEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateCustomerPoints") + return + } + + var req contract.UpdateCustomerPointsRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateCustomerPoints -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateCustomerPoints") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateUpdateCustomerPointsRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::UpdateCustomerPoints -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateCustomerPoints") + return + } + + response, err := h.gamificationService.UpdateCustomerPoints(ctx, id, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateCustomerPoints -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::UpdateCustomerPoints") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::UpdateCustomerPoints") +} + +func (h *GamificationHandler) DeleteCustomerPoints(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteCustomerPoints -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerPointsEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeleteCustomerPoints") + return + } + + err = h.gamificationService.DeleteCustomerPoints(ctx, id) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteCustomerPoints -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::DeleteCustomerPoints") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(nil), "GamificationHandler::DeleteCustomerPoints") +} + +func (h *GamificationHandler) AddCustomerPoints(c *gin.Context) { + ctx := c.Request.Context() + customerIDStr := c.Param("customer_id") + customerID, err := uuid.Parse(customerIDStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::AddCustomerPoints -> invalid customer ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerPointsEntity, "Invalid customer ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::AddCustomerPoints") + return + } + + var req contract.AddCustomerPointsRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::AddCustomerPoints -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::AddCustomerPoints") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateAddCustomerPointsRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::AddCustomerPoints -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::AddCustomerPoints") + return + } + + response, err := h.gamificationService.AddCustomerPoints(ctx, customerID, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::AddCustomerPoints -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::AddCustomerPoints") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::AddCustomerPoints") +} + +func (h *GamificationHandler) DeductCustomerPoints(c *gin.Context) { + ctx := c.Request.Context() + customerIDStr := c.Param("customer_id") + customerID, err := uuid.Parse(customerIDStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeductCustomerPoints -> invalid customer ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerPointsEntity, "Invalid customer ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeductCustomerPoints") + return + } + + var req contract.DeductCustomerPointsRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeductCustomerPoints -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeductCustomerPoints") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateDeductCustomerPointsRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::DeductCustomerPoints -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeductCustomerPoints") + return + } + + response, err := h.gamificationService.DeductCustomerPoints(ctx, customerID, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeductCustomerPoints -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::DeductCustomerPoints") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::DeductCustomerPoints") +} + +// Play Game Handler +func (h *GamificationHandler) PlayGame(c *gin.Context) { + ctx := c.Request.Context() + var req contract.PlayGameRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::PlayGame -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::PlayGame") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidatePlayGameRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::PlayGame -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::PlayGame") + return + } + + response, err := h.gamificationService.PlayGame(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::PlayGame -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GameEntity, err.Error())}), "GamificationHandler::PlayGame") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::PlayGame") +} + +// Additional handler methods for other gamification features +func (h *GamificationHandler) CreateCustomerTokens(c *gin.Context) { + ctx := c.Request.Context() + var req contract.CreateCustomerTokensRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateCustomerTokens -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateCustomerTokens") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateCreateCustomerTokensRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::CreateCustomerTokens -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateCustomerTokens") + return + } + + response, err := h.gamificationService.CreateCustomerTokens(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateCustomerTokens -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::CreateCustomerTokens") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::CreateCustomerTokens") +} + +func (h *GamificationHandler) GetCustomerTokens(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerTokens -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerTokensEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetCustomerTokens") + return + } + + response, err := h.gamificationService.GetCustomerTokens(ctx, id) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerTokens -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::GetCustomerTokens") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetCustomerTokens") +} + +func (h *GamificationHandler) GetCustomerTokensByCustomerIDAndType(c *gin.Context) { + ctx := c.Request.Context() + customerIDStr := c.Param("customer_id") + customerID, err := uuid.Parse(customerIDStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerTokensByCustomerIDAndType -> invalid customer ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerTokensEntity, "Invalid customer ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetCustomerTokensByCustomerIDAndType") + return + } + + tokenType := c.Param("token_type") + + response, err := h.gamificationService.GetCustomerTokensByCustomerIDAndType(ctx, customerID, tokenType) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerTokensByCustomerIDAndType -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::GetCustomerTokensByCustomerIDAndType") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetCustomerTokensByCustomerIDAndType") +} + +func (h *GamificationHandler) ListCustomerTokens(c *gin.Context) { + ctx := c.Request.Context() + var req contract.ListCustomerTokensRequest + if err := c.ShouldBindQuery(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListCustomerTokens -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListCustomerTokens") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateListCustomerTokensRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::ListCustomerTokens -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListCustomerTokens") + return + } + + response, err := h.gamificationService.ListCustomerTokens(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListCustomerTokens -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::ListCustomerTokens") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::ListCustomerTokens") +} + +func (h *GamificationHandler) UpdateCustomerTokens(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateCustomerTokens -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerTokensEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateCustomerTokens") + return + } + + var req contract.UpdateCustomerTokensRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateCustomerTokens -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateCustomerTokens") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateUpdateCustomerTokensRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::UpdateCustomerTokens -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateCustomerTokens") + return + } + + response, err := h.gamificationService.UpdateCustomerTokens(ctx, id, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateCustomerTokens -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::UpdateCustomerTokens") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::UpdateCustomerTokens") +} + +func (h *GamificationHandler) DeleteCustomerTokens(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteCustomerTokens -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerTokensEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeleteCustomerTokens") + return + } + + err = h.gamificationService.DeleteCustomerTokens(ctx, id) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteCustomerTokens -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::DeleteCustomerTokens") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(nil), "GamificationHandler::DeleteCustomerTokens") +} + +func (h *GamificationHandler) AddCustomerTokens(c *gin.Context) { + ctx := c.Request.Context() + customerIDStr := c.Param("customer_id") + customerID, err := uuid.Parse(customerIDStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::AddCustomerTokens -> invalid customer ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerTokensEntity, "Invalid customer ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::AddCustomerTokens") + return + } + + tokenType := c.Param("token_type") + + var req contract.AddCustomerTokensRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::AddCustomerTokens -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::AddCustomerTokens") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateAddCustomerTokensRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::AddCustomerTokens -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::AddCustomerTokens") + return + } + + response, err := h.gamificationService.AddCustomerTokens(ctx, customerID, tokenType, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::AddCustomerTokens -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::AddCustomerTokens") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::AddCustomerTokens") +} + +func (h *GamificationHandler) DeductCustomerTokens(c *gin.Context) { + ctx := c.Request.Context() + customerIDStr := c.Param("customer_id") + customerID, err := uuid.Parse(customerIDStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeductCustomerTokens -> invalid customer ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerTokensEntity, "Invalid customer ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeductCustomerTokens") + return + } + + tokenType := c.Param("token_type") + + var req contract.DeductCustomerTokensRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeductCustomerTokens -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeductCustomerTokens") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateDeductCustomerTokensRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::DeductCustomerTokens -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeductCustomerTokens") + return + } + + response, err := h.gamificationService.DeductCustomerTokens(ctx, customerID, tokenType, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeductCustomerTokens -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::DeductCustomerTokens") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::DeductCustomerTokens") +} diff --git a/internal/handler/gamification_handler_additional.go b/internal/handler/gamification_handler_additional.go new file mode 100644 index 0000000..a5bf803 --- /dev/null +++ b/internal/handler/gamification_handler_additional.go @@ -0,0 +1,709 @@ +package handler + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/util" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// Tier Handlers +func (h *GamificationHandler) CreateTier(c *gin.Context) { + ctx := c.Request.Context() + var req contract.CreateTierRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateTier -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateTier") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateCreateTierRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::CreateTier -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateTier") + return + } + + response, err := h.gamificationService.CreateTier(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateTier -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.TierEntity, err.Error())}), "GamificationHandler::CreateTier") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::CreateTier") +} + +func (h *GamificationHandler) GetTier(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetTier -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.TierEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetTier") + return + } + + response, err := h.gamificationService.GetTier(ctx, id) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetTier -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.TierEntity, err.Error())}), "GamificationHandler::GetTier") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetTier") +} + +func (h *GamificationHandler) ListTiers(c *gin.Context) { + ctx := c.Request.Context() + var req contract.ListTiersRequest + if err := c.ShouldBindQuery(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListTiers -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListTiers") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateListTiersRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::ListTiers -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListTiers") + return + } + + response, err := h.gamificationService.ListTiers(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListTiers -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.TierEntity, err.Error())}), "GamificationHandler::ListTiers") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::ListTiers") +} + +func (h *GamificationHandler) UpdateTier(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateTier -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.TierEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateTier") + return + } + + var req contract.UpdateTierRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateTier -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateTier") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateUpdateTierRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::UpdateTier -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateTier") + return + } + + response, err := h.gamificationService.UpdateTier(ctx, id, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateTier -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.TierEntity, err.Error())}), "GamificationHandler::UpdateTier") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::UpdateTier") +} + +func (h *GamificationHandler) DeleteTier(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteTier -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.TierEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeleteTier") + return + } + + err = h.gamificationService.DeleteTier(ctx, id) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteTier -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.TierEntity, err.Error())}), "GamificationHandler::DeleteTier") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(nil), "GamificationHandler::DeleteTier") +} + +func (h *GamificationHandler) GetTierByPoints(c *gin.Context) { + ctx := c.Request.Context() + pointsStr := c.Param("points") + points, err := strconv.ParseInt(pointsStr, 10, 64) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetTierByPoints -> invalid points") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.TierEntity, "Invalid points format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetTierByPoints") + return + } + + response, err := h.gamificationService.GetTierByPoints(ctx, points) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetTierByPoints -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.TierEntity, err.Error())}), "GamificationHandler::GetTierByPoints") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetTierByPoints") +} + +// Game Handlers +func (h *GamificationHandler) CreateGame(c *gin.Context) { + ctx := c.Request.Context() + var req contract.CreateGameRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateGame -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateGame") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateCreateGameRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::CreateGame -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateGame") + return + } + + response, err := h.gamificationService.CreateGame(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateGame -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GameEntity, err.Error())}), "GamificationHandler::CreateGame") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::CreateGame") +} + +func (h *GamificationHandler) GetGame(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetGame -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.GameEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetGame") + return + } + + response, err := h.gamificationService.GetGame(ctx, id) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetGame -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GameEntity, err.Error())}), "GamificationHandler::GetGame") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetGame") +} + +func (h *GamificationHandler) ListGames(c *gin.Context) { + ctx := c.Request.Context() + var req contract.ListGamesRequest + if err := c.ShouldBindQuery(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListGames -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListGames") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateListGamesRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::ListGames -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListGames") + return + } + + response, err := h.gamificationService.ListGames(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListGames -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GameEntity, err.Error())}), "GamificationHandler::ListGames") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::ListGames") +} + +func (h *GamificationHandler) GetActiveGames(c *gin.Context) { + ctx := c.Request.Context() + response, err := h.gamificationService.GetActiveGames(ctx) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetActiveGames -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GameEntity, err.Error())}), "GamificationHandler::GetActiveGames") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetActiveGames") +} + +func (h *GamificationHandler) UpdateGame(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateGame -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.GameEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateGame") + return + } + + var req contract.UpdateGameRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateGame -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateGame") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateUpdateGameRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::UpdateGame -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateGame") + return + } + + response, err := h.gamificationService.UpdateGame(ctx, id, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateGame -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GameEntity, err.Error())}), "GamificationHandler::UpdateGame") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::UpdateGame") +} + +func (h *GamificationHandler) DeleteGame(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteGame -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.GameEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeleteGame") + return + } + + err = h.gamificationService.DeleteGame(ctx, id) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteGame -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GameEntity, err.Error())}), "GamificationHandler::DeleteGame") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(nil), "GamificationHandler::DeleteGame") +} + +// Game Prize Handlers +func (h *GamificationHandler) CreateGamePrize(c *gin.Context) { + ctx := c.Request.Context() + var req contract.CreateGamePrizeRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateGamePrize -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateGamePrize") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateCreateGamePrizeRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::CreateGamePrize -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateGamePrize") + return + } + + response, err := h.gamificationService.CreateGamePrize(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateGamePrize -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GamePrizeEntity, err.Error())}), "GamificationHandler::CreateGamePrize") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::CreateGamePrize") +} + +func (h *GamificationHandler) GetGamePrize(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetGamePrize -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.GamePrizeEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetGamePrize") + return + } + + response, err := h.gamificationService.GetGamePrize(ctx, id) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetGamePrize -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GamePrizeEntity, err.Error())}), "GamificationHandler::GetGamePrize") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetGamePrize") +} + +func (h *GamificationHandler) ListGamePrizes(c *gin.Context) { + ctx := c.Request.Context() + var req contract.ListGamePrizesRequest + if err := c.ShouldBindQuery(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListGamePrizes -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListGamePrizes") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateListGamePrizesRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::ListGamePrizes -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListGamePrizes") + return + } + + response, err := h.gamificationService.ListGamePrizes(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListGamePrizes -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GamePrizeEntity, err.Error())}), "GamificationHandler::ListGamePrizes") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::ListGamePrizes") +} + +func (h *GamificationHandler) UpdateGamePrize(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateGamePrize -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.GamePrizeEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateGamePrize") + return + } + + var req contract.UpdateGamePrizeRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateGamePrize -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateGamePrize") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateUpdateGamePrizeRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::UpdateGamePrize -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateGamePrize") + return + } + + response, err := h.gamificationService.UpdateGamePrize(ctx, id, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateGamePrize -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GamePrizeEntity, err.Error())}), "GamificationHandler::UpdateGamePrize") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::UpdateGamePrize") +} + +func (h *GamificationHandler) DeleteGamePrize(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteGamePrize -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.GamePrizeEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeleteGamePrize") + return + } + + err = h.gamificationService.DeleteGamePrize(ctx, id) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteGamePrize -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GamePrizeEntity, err.Error())}), "GamificationHandler::DeleteGamePrize") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(nil), "GamificationHandler::DeleteGamePrize") +} + +func (h *GamificationHandler) GetGamePrizesByGameID(c *gin.Context) { + ctx := c.Request.Context() + gameIDStr := c.Param("game_id") + gameID, err := uuid.Parse(gameIDStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetGamePrizesByGameID -> invalid game ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.GamePrizeEntity, "Invalid game ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetGamePrizesByGameID") + return + } + + response, err := h.gamificationService.GetGamePrizesByGameID(ctx, gameID) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetGamePrizesByGameID -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GamePrizeEntity, err.Error())}), "GamificationHandler::GetGamePrizesByGameID") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetGamePrizesByGameID") +} + +func (h *GamificationHandler) GetAvailablePrizes(c *gin.Context) { + ctx := c.Request.Context() + gameIDStr := c.Param("game_id") + gameID, err := uuid.Parse(gameIDStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetAvailablePrizes -> invalid game ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.GamePrizeEntity, "Invalid game ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetAvailablePrizes") + return + } + + response, err := h.gamificationService.GetAvailablePrizes(ctx, gameID) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetAvailablePrizes -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GamePrizeEntity, err.Error())}), "GamificationHandler::GetAvailablePrizes") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetAvailablePrizes") +} + +// Game Play Handlers +func (h *GamificationHandler) CreateGamePlay(c *gin.Context) { + ctx := c.Request.Context() + var req contract.CreateGamePlayRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateGamePlay -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateGamePlay") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateCreateGamePlayRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::CreateGamePlay -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateGamePlay") + return + } + + response, err := h.gamificationService.CreateGamePlay(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateGamePlay -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GamePlayEntity, err.Error())}), "GamificationHandler::CreateGamePlay") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::CreateGamePlay") +} + +func (h *GamificationHandler) GetGamePlay(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetGamePlay -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.GamePlayEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetGamePlay") + return + } + + response, err := h.gamificationService.GetGamePlay(ctx, id) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetGamePlay -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GamePlayEntity, err.Error())}), "GamificationHandler::GetGamePlay") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetGamePlay") +} + +func (h *GamificationHandler) ListGamePlays(c *gin.Context) { + ctx := c.Request.Context() + var req contract.ListGamePlaysRequest + if err := c.ShouldBindQuery(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListGamePlays -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListGamePlays") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateListGamePlaysRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::ListGamePlays -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListGamePlays") + return + } + + response, err := h.gamificationService.ListGamePlays(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListGamePlays -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.GamePlayEntity, err.Error())}), "GamificationHandler::ListGamePlays") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::ListGamePlays") +} + +// Omset Tracker Handlers +func (h *GamificationHandler) CreateOmsetTracker(c *gin.Context) { + ctx := c.Request.Context() + var req contract.CreateOmsetTrackerRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateOmsetTracker -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateOmsetTracker") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateCreateOmsetTrackerRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::CreateOmsetTracker -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateOmsetTracker") + return + } + + response, err := h.gamificationService.CreateOmsetTracker(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateOmsetTracker -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.OmsetTrackerEntity, err.Error())}), "GamificationHandler::CreateOmsetTracker") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::CreateOmsetTracker") +} + +func (h *GamificationHandler) GetOmsetTracker(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetOmsetTracker -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.OmsetTrackerEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetOmsetTracker") + return + } + + response, err := h.gamificationService.GetOmsetTracker(ctx, id) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetOmsetTracker -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.OmsetTrackerEntity, err.Error())}), "GamificationHandler::GetOmsetTracker") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetOmsetTracker") +} + +func (h *GamificationHandler) ListOmsetTrackers(c *gin.Context) { + ctx := c.Request.Context() + var req contract.ListOmsetTrackerRequest + if err := c.ShouldBindQuery(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListOmsetTrackers -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListOmsetTrackers") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateListOmsetTrackerRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::ListOmsetTrackers -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListOmsetTrackers") + return + } + + response, err := h.gamificationService.ListOmsetTrackers(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListOmsetTrackers -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.OmsetTrackerEntity, err.Error())}), "GamificationHandler::ListOmsetTrackers") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::ListOmsetTrackers") +} + +func (h *GamificationHandler) UpdateOmsetTracker(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateOmsetTracker -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.OmsetTrackerEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateOmsetTracker") + return + } + + var req contract.UpdateOmsetTrackerRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateOmsetTracker -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateOmsetTracker") + return + } + + validationError, validationErrorCode := h.gamificationValidator.ValidateUpdateOmsetTrackerRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::UpdateOmsetTracker -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateOmsetTracker") + return + } + + response, err := h.gamificationService.UpdateOmsetTracker(ctx, id, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateOmsetTracker -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.OmsetTrackerEntity, err.Error())}), "GamificationHandler::UpdateOmsetTracker") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::UpdateOmsetTracker") +} + +func (h *GamificationHandler) DeleteOmsetTracker(c *gin.Context) { + ctx := c.Request.Context() + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteOmsetTracker -> invalid ID") + validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.OmsetTrackerEntity, "Invalid ID format") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeleteOmsetTracker") + return + } + + err = h.gamificationService.DeleteOmsetTracker(ctx, id) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteOmsetTracker -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.OmsetTrackerEntity, err.Error())}), "GamificationHandler::DeleteOmsetTracker") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(nil), "GamificationHandler::DeleteOmsetTracker") +} diff --git a/internal/mappers/customer_points_mapper.go b/internal/mappers/customer_points_mapper.go new file mode 100644 index 0000000..0cf67ba --- /dev/null +++ b/internal/mappers/customer_points_mapper.go @@ -0,0 +1,46 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +// ToCustomerPointsResponse converts a customer points entity to a customer points response +func ToCustomerPointsResponse(customerPoints *entities.CustomerPoints) *models.CustomerPointsResponse { + if customerPoints == nil { + return nil + } + + return &models.CustomerPointsResponse{ + ID: customerPoints.ID, + CustomerID: customerPoints.CustomerID, + Balance: customerPoints.Balance, + Customer: ToCustomerResponse(&customerPoints.Customer), + CreatedAt: customerPoints.CreatedAt, + UpdatedAt: customerPoints.UpdatedAt, + } +} + +// ToCustomerPointsResponses converts a slice of customer points entities to customer points responses +func ToCustomerPointsResponses(customerPoints []entities.CustomerPoints) []models.CustomerPointsResponse { + responses := make([]models.CustomerPointsResponse, len(customerPoints)) + for i, cp := range customerPoints { + responses[i] = *ToCustomerPointsResponse(&cp) + } + return responses +} + +// ToCustomerPointsEntity converts a create customer points request to a customer points entity +func ToCustomerPointsEntity(req *models.CreateCustomerPointsRequest) *entities.CustomerPoints { + return &entities.CustomerPoints{ + CustomerID: req.CustomerID, + Balance: req.Balance, + } +} + +// UpdateCustomerPointsEntity updates a customer points entity with update request data +func UpdateCustomerPointsEntity(customerPoints *entities.CustomerPoints, req *models.UpdateCustomerPointsRequest) { + if req.Balance >= 0 { + customerPoints.Balance = req.Balance + } +} diff --git a/internal/mappers/customer_tokens_mapper.go b/internal/mappers/customer_tokens_mapper.go new file mode 100644 index 0000000..e88cfed --- /dev/null +++ b/internal/mappers/customer_tokens_mapper.go @@ -0,0 +1,48 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +// ToCustomerTokensResponse converts a customer tokens entity to a customer tokens response +func ToCustomerTokensResponse(customerTokens *entities.CustomerTokens) *models.CustomerTokensResponse { + if customerTokens == nil { + return nil + } + + return &models.CustomerTokensResponse{ + ID: customerTokens.ID, + CustomerID: customerTokens.CustomerID, + TokenType: string(customerTokens.TokenType), + Balance: customerTokens.Balance, + Customer: ToCustomerResponse(&customerTokens.Customer), + CreatedAt: customerTokens.CreatedAt, + UpdatedAt: customerTokens.UpdatedAt, + } +} + +// ToCustomerTokensResponses converts a slice of customer tokens entities to customer tokens responses +func ToCustomerTokensResponses(customerTokens []entities.CustomerTokens) []models.CustomerTokensResponse { + responses := make([]models.CustomerTokensResponse, len(customerTokens)) + for i, ct := range customerTokens { + responses[i] = *ToCustomerTokensResponse(&ct) + } + return responses +} + +// ToCustomerTokensEntity converts a create customer tokens request to a customer tokens entity +func ToCustomerTokensEntity(req *models.CreateCustomerTokensRequest) *entities.CustomerTokens { + return &entities.CustomerTokens{ + CustomerID: req.CustomerID, + TokenType: entities.TokenType(req.TokenType), + Balance: req.Balance, + } +} + +// UpdateCustomerTokensEntity updates a customer tokens entity with update request data +func UpdateCustomerTokensEntity(customerTokens *entities.CustomerTokens, req *models.UpdateCustomerTokensRequest) { + if req.Balance >= 0 { + customerTokens.Balance = req.Balance + } +} diff --git a/internal/mappers/game_mapper.go b/internal/mappers/game_mapper.go new file mode 100644 index 0000000..5e7001f --- /dev/null +++ b/internal/mappers/game_mapper.go @@ -0,0 +1,58 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +// ToGameResponse converts a game entity to a game response +func ToGameResponse(game *entities.Game) *models.GameResponse { + if game == nil { + return nil + } + + return &models.GameResponse{ + ID: game.ID, + Name: game.Name, + Type: string(game.Type), + IsActive: game.IsActive, + Metadata: game.Metadata, + CreatedAt: game.CreatedAt, + UpdatedAt: game.UpdatedAt, + } +} + +// ToGameResponses converts a slice of game entities to game responses +func ToGameResponses(games []entities.Game) []models.GameResponse { + responses := make([]models.GameResponse, len(games)) + for i, game := range games { + responses[i] = *ToGameResponse(&game) + } + return responses +} + +// ToGameEntity converts a create game request to a game entity +func ToGameEntity(req *models.CreateGameRequest) *entities.Game { + return &entities.Game{ + Name: req.Name, + Type: entities.GameType(req.Type), + IsActive: req.IsActive, + Metadata: req.Metadata, + } +} + +// UpdateGameEntity updates a game entity with update request data +func UpdateGameEntity(game *entities.Game, req *models.UpdateGameRequest) { + if req.Name != nil { + game.Name = *req.Name + } + if req.Type != nil { + game.Type = entities.GameType(*req.Type) + } + if req.IsActive != nil { + game.IsActive = *req.IsActive + } + if req.Metadata != nil { + game.Metadata = req.Metadata + } +} diff --git a/internal/mappers/game_play_mapper.go b/internal/mappers/game_play_mapper.go new file mode 100644 index 0000000..b64c532 --- /dev/null +++ b/internal/mappers/game_play_mapper.go @@ -0,0 +1,45 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +// ToGamePlayResponse converts a game play entity to a game play response +func ToGamePlayResponse(gamePlay *entities.GamePlay) *models.GamePlayResponse { + if gamePlay == nil { + return nil + } + + return &models.GamePlayResponse{ + ID: gamePlay.ID, + GameID: gamePlay.GameID, + CustomerID: gamePlay.CustomerID, + PrizeID: gamePlay.PrizeID, + TokenUsed: gamePlay.TokenUsed, + RandomSeed: gamePlay.RandomSeed, + CreatedAt: gamePlay.CreatedAt, + Game: ToGameResponse(&gamePlay.Game), + Customer: ToCustomerResponse(&gamePlay.Customer), + Prize: ToGamePrizeResponse(gamePlay.Prize), + } +} + +// ToGamePlayResponses converts a slice of game play entities to game play responses +func ToGamePlayResponses(gamePlays []entities.GamePlay) []models.GamePlayResponse { + responses := make([]models.GamePlayResponse, len(gamePlays)) + for i, gp := range gamePlays { + responses[i] = *ToGamePlayResponse(&gp) + } + return responses +} + +// ToGamePlayEntity converts a create game play request to a game play entity +func ToGamePlayEntity(req *models.CreateGamePlayRequest) *entities.GamePlay { + return &entities.GamePlay{ + GameID: req.GameID, + CustomerID: req.CustomerID, + TokenUsed: req.TokenUsed, + RandomSeed: req.RandomSeed, + } +} diff --git a/internal/mappers/game_prize_mapper.go b/internal/mappers/game_prize_mapper.go new file mode 100644 index 0000000..60c0e23 --- /dev/null +++ b/internal/mappers/game_prize_mapper.go @@ -0,0 +1,77 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +// ToGamePrizeResponse converts a game prize entity to a game prize response +func ToGamePrizeResponse(gamePrize *entities.GamePrize) *models.GamePrizeResponse { + if gamePrize == nil { + return nil + } + + return &models.GamePrizeResponse{ + ID: gamePrize.ID, + GameID: gamePrize.GameID, + Name: gamePrize.Name, + Weight: gamePrize.Weight, + Stock: gamePrize.Stock, + MaxStock: gamePrize.MaxStock, + Threshold: gamePrize.Threshold, + FallbackPrizeID: gamePrize.FallbackPrizeID, + Metadata: gamePrize.Metadata, + Game: ToGameResponse(&gamePrize.Game), + FallbackPrize: ToGamePrizeResponse(gamePrize.FallbackPrize), + CreatedAt: gamePrize.CreatedAt, + UpdatedAt: gamePrize.UpdatedAt, + } +} + +// ToGamePrizeResponses converts a slice of game prize entities to game prize responses +func ToGamePrizeResponses(gamePrizes []entities.GamePrize) []models.GamePrizeResponse { + responses := make([]models.GamePrizeResponse, len(gamePrizes)) + for i, gp := range gamePrizes { + responses[i] = *ToGamePrizeResponse(&gp) + } + return responses +} + +// ToGamePrizeEntity converts a create game prize request to a game prize entity +func ToGamePrizeEntity(req *models.CreateGamePrizeRequest) *entities.GamePrize { + return &entities.GamePrize{ + GameID: req.GameID, + Name: req.Name, + Weight: req.Weight, + Stock: req.Stock, + MaxStock: req.MaxStock, + Threshold: req.Threshold, + FallbackPrizeID: req.FallbackPrizeID, + Metadata: req.Metadata, + } +} + +// UpdateGamePrizeEntity updates a game prize entity with update request data +func UpdateGamePrizeEntity(gamePrize *entities.GamePrize, req *models.UpdateGamePrizeRequest) { + if req.Name != nil { + gamePrize.Name = *req.Name + } + if req.Weight != nil { + gamePrize.Weight = *req.Weight + } + if req.Stock != nil { + gamePrize.Stock = *req.Stock + } + if req.MaxStock != nil { + gamePrize.MaxStock = req.MaxStock + } + if req.Threshold != nil { + gamePrize.Threshold = req.Threshold + } + if req.FallbackPrizeID != nil { + gamePrize.FallbackPrizeID = req.FallbackPrizeID + } + if req.Metadata != nil { + gamePrize.Metadata = req.Metadata + } +} diff --git a/internal/mappers/omset_tracker_mapper.go b/internal/mappers/omset_tracker_mapper.go new file mode 100644 index 0000000..dacf25b --- /dev/null +++ b/internal/mappers/omset_tracker_mapper.go @@ -0,0 +1,64 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +// ToOmsetTrackerResponse converts an omset tracker entity to an omset tracker response +func ToOmsetTrackerResponse(omsetTracker *entities.OmsetTracker) *models.OmsetTrackerResponse { + if omsetTracker == nil { + return nil + } + + return &models.OmsetTrackerResponse{ + ID: omsetTracker.ID, + PeriodType: string(omsetTracker.PeriodType), + PeriodStart: omsetTracker.PeriodStart, + PeriodEnd: omsetTracker.PeriodEnd, + Total: omsetTracker.Total, + GameID: omsetTracker.GameID, + Game: ToGameResponse(omsetTracker.Game), + CreatedAt: omsetTracker.CreatedAt, + UpdatedAt: omsetTracker.UpdatedAt, + } +} + +// ToOmsetTrackerResponses converts a slice of omset tracker entities to omset tracker responses +func ToOmsetTrackerResponses(omsetTrackers []entities.OmsetTracker) []models.OmsetTrackerResponse { + responses := make([]models.OmsetTrackerResponse, len(omsetTrackers)) + for i, ot := range omsetTrackers { + responses[i] = *ToOmsetTrackerResponse(&ot) + } + return responses +} + +// ToOmsetTrackerEntity converts a create omset tracker request to an omset tracker entity +func ToOmsetTrackerEntity(req *models.CreateOmsetTrackerRequest) *entities.OmsetTracker { + return &entities.OmsetTracker{ + PeriodType: entities.PeriodType(req.PeriodType), + PeriodStart: req.PeriodStart, + PeriodEnd: req.PeriodEnd, + Total: req.Total, + GameID: req.GameID, + } +} + +// UpdateOmsetTrackerEntity updates an omset tracker entity with update request data +func UpdateOmsetTrackerEntity(omsetTracker *entities.OmsetTracker, req *models.UpdateOmsetTrackerRequest) { + if req.PeriodType != nil { + omsetTracker.PeriodType = entities.PeriodType(*req.PeriodType) + } + if req.PeriodStart != nil { + omsetTracker.PeriodStart = *req.PeriodStart + } + if req.PeriodEnd != nil { + omsetTracker.PeriodEnd = *req.PeriodEnd + } + if req.Total != nil { + omsetTracker.Total = *req.Total + } + if req.GameID != nil { + omsetTracker.GameID = req.GameID + } +} diff --git a/internal/mappers/tier_mapper.go b/internal/mappers/tier_mapper.go new file mode 100644 index 0000000..8bb1af8 --- /dev/null +++ b/internal/mappers/tier_mapper.go @@ -0,0 +1,53 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +// ToTierResponse converts a tier entity to a tier response +func ToTierResponse(tier *entities.Tier) *models.TierResponse { + if tier == nil { + return nil + } + + return &models.TierResponse{ + ID: tier.ID, + Name: tier.Name, + MinPoints: tier.MinPoints, + Benefits: tier.Benefits, + CreatedAt: tier.CreatedAt, + UpdatedAt: tier.UpdatedAt, + } +} + +// ToTierResponses converts a slice of tier entities to tier responses +func ToTierResponses(tiers []entities.Tier) []models.TierResponse { + responses := make([]models.TierResponse, len(tiers)) + for i, tier := range tiers { + responses[i] = *ToTierResponse(&tier) + } + return responses +} + +// ToTierEntity converts a create tier request to a tier entity +func ToTierEntity(req *models.CreateTierRequest) *entities.Tier { + return &entities.Tier{ + Name: req.Name, + MinPoints: req.MinPoints, + Benefits: req.Benefits, + } +} + +// UpdateTierEntity updates a tier entity with update request data +func UpdateTierEntity(tier *entities.Tier, req *models.UpdateTierRequest) { + if req.Name != nil { + tier.Name = *req.Name + } + if req.MinPoints != nil { + tier.MinPoints = *req.MinPoints + } + if req.Benefits != nil { + tier.Benefits = req.Benefits + } +} diff --git a/internal/models/customer_points.go b/internal/models/customer_points.go new file mode 100644 index 0000000..7241b68 --- /dev/null +++ b/internal/models/customer_points.go @@ -0,0 +1,41 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateCustomerPointsRequest struct { + CustomerID uuid.UUID `json:"customer_id" validate:"required"` + Balance int64 `json:"balance" validate:"min=0"` +} + +type UpdateCustomerPointsRequest struct { + Balance int64 `json:"balance" validate:"min=0"` +} + +type AddCustomerPointsRequest struct { + Points int64 `json:"points" validate:"required,min=1"` +} + +type DeductCustomerPointsRequest struct { + Points int64 `json:"points" validate:"required,min=1"` +} + +type CustomerPointsResponse struct { + ID uuid.UUID `json:"id"` + CustomerID uuid.UUID `json:"customer_id"` + Balance int64 `json:"balance"` + Customer *CustomerResponse `json:"customer,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListCustomerPointsQuery struct { + Page int `query:"page" validate:"min=1"` + Limit int `query:"limit" validate:"min=1,max=100"` + Search string `query:"search"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=balance created_at updated_at"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` +} diff --git a/internal/models/customer_tokens.go b/internal/models/customer_tokens.go new file mode 100644 index 0000000..7ee4455 --- /dev/null +++ b/internal/models/customer_tokens.go @@ -0,0 +1,44 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateCustomerTokensRequest struct { + CustomerID uuid.UUID `json:"customer_id" validate:"required"` + TokenType string `json:"token_type" validate:"required,oneof=SPIN RAFFLE MINIGAME"` + Balance int64 `json:"balance" validate:"min=0"` +} + +type UpdateCustomerTokensRequest struct { + Balance int64 `json:"balance" validate:"min=0"` +} + +type AddCustomerTokensRequest struct { + Tokens int64 `json:"tokens" validate:"required,min=1"` +} + +type DeductCustomerTokensRequest struct { + Tokens int64 `json:"tokens" validate:"required,min=1"` +} + +type CustomerTokensResponse struct { + ID uuid.UUID `json:"id"` + CustomerID uuid.UUID `json:"customer_id"` + TokenType string `json:"token_type"` + Balance int64 `json:"balance"` + Customer *CustomerResponse `json:"customer,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListCustomerTokensQuery struct { + Page int `query:"page" validate:"min=1"` + Limit int `query:"limit" validate:"min=1,max=100"` + Search string `query:"search"` + TokenType string `query:"token_type" validate:"omitempty,oneof=SPIN RAFFLE MINIGAME"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=balance token_type created_at updated_at"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` +} diff --git a/internal/models/game.go b/internal/models/game.go new file mode 100644 index 0000000..cd7f637 --- /dev/null +++ b/internal/models/game.go @@ -0,0 +1,41 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateGameRequest struct { + Name string `json:"name" validate:"required"` + Type string `json:"type" validate:"required,oneof=SPIN RAFFLE MINIGAME"` + IsActive bool `json:"is_active"` + Metadata map[string]interface{} `json:"metadata"` +} + +type UpdateGameRequest struct { + Name *string `json:"name,omitempty" validate:"omitempty,required"` + Type *string `json:"type,omitempty" validate:"omitempty,oneof=SPIN RAFFLE MINIGAME"` + IsActive *bool `json:"is_active,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type GameResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsActive bool `json:"is_active"` + Metadata map[string]interface{} `json:"metadata"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListGamesQuery struct { + Page int `query:"page" validate:"min=1"` + Limit int `query:"limit" validate:"min=1,max=100"` + Search string `query:"search"` + Type string `query:"type" validate:"omitempty,oneof=SPIN RAFFLE MINIGAME"` + IsActive *bool `query:"is_active"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=name type created_at updated_at"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` +} diff --git a/internal/models/game_play.go b/internal/models/game_play.go new file mode 100644 index 0000000..342c2c9 --- /dev/null +++ b/internal/models/game_play.go @@ -0,0 +1,50 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateGamePlayRequest struct { + GameID uuid.UUID `json:"game_id" validate:"required"` + CustomerID uuid.UUID `json:"customer_id" validate:"required"` + TokenUsed int `json:"token_used" validate:"min=0"` + RandomSeed *string `json:"random_seed,omitempty"` +} + +type GamePlayResponse struct { + ID uuid.UUID `json:"id"` + GameID uuid.UUID `json:"game_id"` + CustomerID uuid.UUID `json:"customer_id"` + PrizeID *uuid.UUID `json:"prize_id,omitempty"` + TokenUsed int `json:"token_used"` + RandomSeed *string `json:"random_seed,omitempty"` + CreatedAt time.Time `json:"created_at"` + Game *GameResponse `json:"game,omitempty"` + Customer *CustomerResponse `json:"customer,omitempty"` + Prize *GamePrizeResponse `json:"prize,omitempty"` +} + +type ListGamePlaysQuery struct { + Page int `query:"page" validate:"min=1"` + Limit int `query:"limit" validate:"min=1,max=100"` + Search string `query:"search"` + GameID *uuid.UUID `query:"game_id"` + CustomerID *uuid.UUID `query:"customer_id"` + PrizeID *uuid.UUID `query:"prize_id"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=created_at token_used"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` +} + +type PlayGameRequest struct { + GameID uuid.UUID `json:"game_id" validate:"required"` + CustomerID uuid.UUID `json:"customer_id" validate:"required"` + TokenUsed int `json:"token_used" validate:"min=0"` +} + +type PlayGameResponse struct { + GamePlay GamePlayResponse `json:"game_play"` + PrizeWon *GamePrizeResponse `json:"prize_won,omitempty"` + TokensRemaining int64 `json:"tokens_remaining"` +} diff --git a/internal/models/game_prize.go b/internal/models/game_prize.go new file mode 100644 index 0000000..6b99a24 --- /dev/null +++ b/internal/models/game_prize.go @@ -0,0 +1,53 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateGamePrizeRequest struct { + GameID uuid.UUID `json:"game_id" validate:"required"` + Name string `json:"name" validate:"required"` + Weight int `json:"weight" validate:"min=1"` + Stock int `json:"stock" validate:"min=0"` + MaxStock *int `json:"max_stock,omitempty"` + Threshold *int64 `json:"threshold,omitempty"` + FallbackPrizeID *uuid.UUID `json:"fallback_prize_id,omitempty"` + Metadata map[string]interface{} `json:"metadata"` +} + +type UpdateGamePrizeRequest struct { + Name *string `json:"name,omitempty" validate:"omitempty,required"` + Weight *int `json:"weight,omitempty" validate:"omitempty,min=1"` + Stock *int `json:"stock,omitempty" validate:"omitempty,min=0"` + MaxStock *int `json:"max_stock,omitempty"` + Threshold *int64 `json:"threshold,omitempty"` + FallbackPrizeID *uuid.UUID `json:"fallback_prize_id,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type GamePrizeResponse struct { + ID uuid.UUID `json:"id"` + GameID uuid.UUID `json:"game_id"` + Name string `json:"name"` + Weight int `json:"weight"` + Stock int `json:"stock"` + MaxStock *int `json:"max_stock,omitempty"` + Threshold *int64 `json:"threshold,omitempty"` + FallbackPrizeID *uuid.UUID `json:"fallback_prize_id,omitempty"` + Metadata map[string]interface{} `json:"metadata"` + Game *GameResponse `json:"game,omitempty"` + FallbackPrize *GamePrizeResponse `json:"fallback_prize,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListGamePrizesQuery struct { + Page int `query:"page" validate:"min=1"` + Limit int `query:"limit" validate:"min=1,max=100"` + Search string `query:"search"` + GameID *uuid.UUID `query:"game_id"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=name weight stock created_at updated_at"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` +} diff --git a/internal/models/omset_tracker.go b/internal/models/omset_tracker.go new file mode 100644 index 0000000..d364663 --- /dev/null +++ b/internal/models/omset_tracker.go @@ -0,0 +1,51 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateOmsetTrackerRequest struct { + PeriodType string `json:"period_type" validate:"required,oneof=DAILY WEEKLY MONTHLY TOTAL"` + PeriodStart time.Time `json:"period_start" validate:"required"` + PeriodEnd time.Time `json:"period_end" validate:"required"` + Total int64 `json:"total" validate:"min=0"` + GameID *uuid.UUID `json:"game_id,omitempty"` +} + +type UpdateOmsetTrackerRequest struct { + PeriodType *string `json:"period_type,omitempty" validate:"omitempty,oneof=DAILY WEEKLY MONTHLY TOTAL"` + PeriodStart *time.Time `json:"period_start,omitempty"` + PeriodEnd *time.Time `json:"period_end,omitempty"` + Total *int64 `json:"total,omitempty" validate:"omitempty,min=0"` + GameID *uuid.UUID `json:"game_id,omitempty"` +} + +type AddOmsetRequest struct { + Amount int64 `json:"amount" validate:"required,min=1"` +} + +type OmsetTrackerResponse struct { + ID uuid.UUID `json:"id"` + PeriodType string `json:"period_type"` + PeriodStart time.Time `json:"period_start"` + PeriodEnd time.Time `json:"period_end"` + Total int64 `json:"total"` + GameID *uuid.UUID `json:"game_id,omitempty"` + Game *GameResponse `json:"game,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListOmsetTrackerQuery struct { + Page int `query:"page" validate:"min=1"` + Limit int `query:"limit" validate:"min=1,max=100"` + Search string `query:"search"` + PeriodType string `query:"period_type" validate:"omitempty,oneof=DAILY WEEKLY MONTHLY TOTAL"` + GameID *uuid.UUID `query:"game_id"` + From *time.Time `query:"from"` + To *time.Time `query:"to"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=period_type period_start total created_at updated_at"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` +} diff --git a/internal/models/tier.go b/internal/models/tier.go new file mode 100644 index 0000000..791c5ed --- /dev/null +++ b/internal/models/tier.go @@ -0,0 +1,36 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateTierRequest struct { + Name string `json:"name" validate:"required"` + MinPoints int64 `json:"min_points" validate:"min=0"` + Benefits map[string]interface{} `json:"benefits"` +} + +type UpdateTierRequest struct { + Name *string `json:"name,omitempty" validate:"omitempty,required"` + MinPoints *int64 `json:"min_points,omitempty" validate:"omitempty,min=0"` + Benefits map[string]interface{} `json:"benefits,omitempty"` +} + +type TierResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + MinPoints int64 `json:"min_points"` + Benefits map[string]interface{} `json:"benefits"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListTiersQuery struct { + Page int `query:"page" validate:"min=1"` + Limit int `query:"limit" validate:"min=1,max=100"` + Search string `query:"search"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=name min_points created_at updated_at"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` +} diff --git a/internal/processor/customer_points_processor.go b/internal/processor/customer_points_processor.go new file mode 100644 index 0000000..fc9029d --- /dev/null +++ b/internal/processor/customer_points_processor.go @@ -0,0 +1,196 @@ +package processor + +import ( + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" + "context" + "errors" + "fmt" + + "github.com/google/uuid" +) + +type CustomerPointsProcessor struct { + customerPointsRepo *repository.CustomerPointsRepository +} + +func NewCustomerPointsProcessor(customerPointsRepo *repository.CustomerPointsRepository) *CustomerPointsProcessor { + return &CustomerPointsProcessor{ + customerPointsRepo: customerPointsRepo, + } +} + +// CreateCustomerPoints creates a new customer points record +func (p *CustomerPointsProcessor) CreateCustomerPoints(ctx context.Context, req *models.CreateCustomerPointsRequest) (*models.CustomerPointsResponse, error) { + // Convert request to entity + customerPoints := mappers.ToCustomerPointsEntity(req) + + // Create customer points + err := p.customerPointsRepo.Create(ctx, customerPoints) + if err != nil { + return nil, fmt.Errorf("failed to create customer points: %w", err) + } + + return mappers.ToCustomerPointsResponse(customerPoints), nil +} + +// GetCustomerPoints retrieves customer points by ID +func (p *CustomerPointsProcessor) GetCustomerPoints(ctx context.Context, id uuid.UUID) (*models.CustomerPointsResponse, error) { + customerPoints, err := p.customerPointsRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("customer points not found: %w", err) + } + + return mappers.ToCustomerPointsResponse(customerPoints), nil +} + +// GetCustomerPointsByCustomerID retrieves customer points by customer ID +func (p *CustomerPointsProcessor) GetCustomerPointsByCustomerID(ctx context.Context, customerID uuid.UUID) (*models.CustomerPointsResponse, error) { + customerPoints, err := p.customerPointsRepo.EnsureCustomerPoints(ctx, customerID) + if err != nil { + return nil, fmt.Errorf("failed to get customer points: %w", err) + } + + return mappers.ToCustomerPointsResponse(customerPoints), nil +} + +// ListCustomerPoints retrieves customer points with pagination and filtering +func (p *CustomerPointsProcessor) ListCustomerPoints(ctx context.Context, query *models.ListCustomerPointsQuery) (*models.PaginatedResponse[models.CustomerPointsResponse], error) { + // Set default values + if query.Page <= 0 { + query.Page = 1 + } + if query.Limit <= 0 { + query.Limit = 10 + } + if query.Limit > 100 { + query.Limit = 100 + } + + offset := (query.Page - 1) * query.Limit + + // Get customer points from repository + customerPoints, total, err := p.customerPointsRepo.List( + ctx, + offset, + query.Limit, + query.Search, + query.SortBy, + query.SortOrder, + ) + if err != nil { + return nil, fmt.Errorf("failed to list customer points: %w", err) + } + + // Convert to responses + responses := mappers.ToCustomerPointsResponses(customerPoints) + + // Calculate pagination info + totalPages := int((total + int64(query.Limit) - 1) / int64(query.Limit)) + + return &models.PaginatedResponse[models.CustomerPointsResponse]{ + Data: responses, + Pagination: models.Pagination{ + Page: query.Page, + Limit: query.Limit, + Total: total, + TotalPages: totalPages, + }, + }, nil +} + +// UpdateCustomerPoints updates an existing customer points record +func (p *CustomerPointsProcessor) UpdateCustomerPoints(ctx context.Context, id uuid.UUID, req *models.UpdateCustomerPointsRequest) (*models.CustomerPointsResponse, error) { + // Get existing customer points + customerPoints, err := p.customerPointsRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("customer points not found: %w", err) + } + + // Update customer points fields + mappers.UpdateCustomerPointsEntity(customerPoints, req) + + // Save updated customer points + err = p.customerPointsRepo.Update(ctx, customerPoints) + if err != nil { + return nil, fmt.Errorf("failed to update customer points: %w", err) + } + + return mappers.ToCustomerPointsResponse(customerPoints), nil +} + +// DeleteCustomerPoints deletes a customer points record +func (p *CustomerPointsProcessor) DeleteCustomerPoints(ctx context.Context, id uuid.UUID) error { + // Get existing customer points + _, err := p.customerPointsRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("customer points not found: %w", err) + } + + // Delete customer points + err = p.customerPointsRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete customer points: %w", err) + } + + return nil +} + +// AddPoints adds points to a customer's balance +func (p *CustomerPointsProcessor) AddPoints(ctx context.Context, customerID uuid.UUID, points int64) (*models.CustomerPointsResponse, error) { + if points <= 0 { + return nil, errors.New("points must be greater than 0") + } + + // Ensure customer points record exists + _, err := p.customerPointsRepo.EnsureCustomerPoints(ctx, customerID) + if err != nil { + return nil, fmt.Errorf("failed to ensure customer points: %w", err) + } + + // Add points + err = p.customerPointsRepo.AddPoints(ctx, customerID, points) + if err != nil { + return nil, fmt.Errorf("failed to add points: %w", err) + } + + // Get updated customer points + customerPoints, err := p.customerPointsRepo.GetByCustomerID(ctx, customerID) + if err != nil { + return nil, fmt.Errorf("failed to get updated customer points: %w", err) + } + + return mappers.ToCustomerPointsResponse(customerPoints), nil +} + +// DeductPoints deducts points from a customer's balance +func (p *CustomerPointsProcessor) DeductPoints(ctx context.Context, customerID uuid.UUID, points int64) (*models.CustomerPointsResponse, error) { + if points <= 0 { + return nil, errors.New("points must be greater than 0") + } + + // Get current customer points + customerPoints, err := p.customerPointsRepo.GetByCustomerID(ctx, customerID) + if err != nil { + return nil, fmt.Errorf("customer points not found: %w", err) + } + + if customerPoints.Balance < points { + return nil, errors.New("insufficient points balance") + } + + // Deduct points + err = p.customerPointsRepo.DeductPoints(ctx, customerID, points) + if err != nil { + return nil, fmt.Errorf("failed to deduct points: %w", err) + } + + // Get updated customer points + updatedCustomerPoints, err := p.customerPointsRepo.GetByCustomerID(ctx, customerID) + if err != nil { + return nil, fmt.Errorf("failed to get updated customer points: %w", err) + } + + return mappers.ToCustomerPointsResponse(updatedCustomerPoints), nil +} diff --git a/internal/processor/customer_tokens_processor.go b/internal/processor/customer_tokens_processor.go new file mode 100644 index 0000000..f66e693 --- /dev/null +++ b/internal/processor/customer_tokens_processor.go @@ -0,0 +1,198 @@ +package processor + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" + "context" + "errors" + "fmt" + + "github.com/google/uuid" +) + +type CustomerTokensProcessor struct { + customerTokensRepo *repository.CustomerTokensRepository +} + +func NewCustomerTokensProcessor(customerTokensRepo *repository.CustomerTokensRepository) *CustomerTokensProcessor { + return &CustomerTokensProcessor{ + customerTokensRepo: customerTokensRepo, + } +} + +// CreateCustomerTokens creates a new customer tokens record +func (p *CustomerTokensProcessor) CreateCustomerTokens(ctx context.Context, req *models.CreateCustomerTokensRequest) (*models.CustomerTokensResponse, error) { + // Convert request to entity + customerTokens := mappers.ToCustomerTokensEntity(req) + + // Create customer tokens + err := p.customerTokensRepo.Create(ctx, customerTokens) + if err != nil { + return nil, fmt.Errorf("failed to create customer tokens: %w", err) + } + + return mappers.ToCustomerTokensResponse(customerTokens), nil +} + +// GetCustomerTokens retrieves customer tokens by ID +func (p *CustomerTokensProcessor) GetCustomerTokens(ctx context.Context, id uuid.UUID) (*models.CustomerTokensResponse, error) { + customerTokens, err := p.customerTokensRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("customer tokens not found: %w", err) + } + + return mappers.ToCustomerTokensResponse(customerTokens), nil +} + +// GetCustomerTokensByCustomerIDAndType retrieves customer tokens by customer ID and token type +func (p *CustomerTokensProcessor) GetCustomerTokensByCustomerIDAndType(ctx context.Context, customerID uuid.UUID, tokenType string) (*models.CustomerTokensResponse, error) { + customerTokens, err := p.customerTokensRepo.EnsureCustomerTokens(ctx, customerID, entities.TokenType(tokenType)) + if err != nil { + return nil, fmt.Errorf("failed to get customer tokens: %w", err) + } + + return mappers.ToCustomerTokensResponse(customerTokens), nil +} + +// ListCustomerTokens retrieves customer tokens with pagination and filtering +func (p *CustomerTokensProcessor) ListCustomerTokens(ctx context.Context, query *models.ListCustomerTokensQuery) (*models.PaginatedResponse[models.CustomerTokensResponse], error) { + // Set default values + if query.Page <= 0 { + query.Page = 1 + } + if query.Limit <= 0 { + query.Limit = 10 + } + if query.Limit > 100 { + query.Limit = 100 + } + + offset := (query.Page - 1) * query.Limit + + // Get customer tokens from repository + customerTokens, total, err := p.customerTokensRepo.List( + ctx, + offset, + query.Limit, + query.Search, + query.TokenType, + query.SortBy, + query.SortOrder, + ) + if err != nil { + return nil, fmt.Errorf("failed to list customer tokens: %w", err) + } + + // Convert to responses + responses := mappers.ToCustomerTokensResponses(customerTokens) + + // Calculate pagination info + totalPages := int((total + int64(query.Limit) - 1) / int64(query.Limit)) + + return &models.PaginatedResponse[models.CustomerTokensResponse]{ + Data: responses, + Pagination: models.Pagination{ + Page: query.Page, + Limit: query.Limit, + Total: total, + TotalPages: totalPages, + }, + }, nil +} + +// UpdateCustomerTokens updates an existing customer tokens record +func (p *CustomerTokensProcessor) UpdateCustomerTokens(ctx context.Context, id uuid.UUID, req *models.UpdateCustomerTokensRequest) (*models.CustomerTokensResponse, error) { + // Get existing customer tokens + customerTokens, err := p.customerTokensRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("customer tokens not found: %w", err) + } + + // Update customer tokens fields + mappers.UpdateCustomerTokensEntity(customerTokens, req) + + // Save updated customer tokens + err = p.customerTokensRepo.Update(ctx, customerTokens) + if err != nil { + return nil, fmt.Errorf("failed to update customer tokens: %w", err) + } + + return mappers.ToCustomerTokensResponse(customerTokens), nil +} + +// DeleteCustomerTokens deletes a customer tokens record +func (p *CustomerTokensProcessor) DeleteCustomerTokens(ctx context.Context, id uuid.UUID) error { + // Get existing customer tokens + _, err := p.customerTokensRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("customer tokens not found: %w", err) + } + + // Delete customer tokens + err = p.customerTokensRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete customer tokens: %w", err) + } + + return nil +} + +// AddTokens adds tokens to a customer's balance +func (p *CustomerTokensProcessor) AddTokens(ctx context.Context, customerID uuid.UUID, tokenType string, tokens int64) (*models.CustomerTokensResponse, error) { + if tokens <= 0 { + return nil, errors.New("tokens must be greater than 0") + } + + // Ensure customer tokens record exists + _, err := p.customerTokensRepo.EnsureCustomerTokens(ctx, customerID, entities.TokenType(tokenType)) + if err != nil { + return nil, fmt.Errorf("failed to ensure customer tokens: %w", err) + } + + // Add tokens + err = p.customerTokensRepo.AddTokens(ctx, customerID, entities.TokenType(tokenType), tokens) + if err != nil { + return nil, fmt.Errorf("failed to add tokens: %w", err) + } + + // Get updated customer tokens + customerTokens, err := p.customerTokensRepo.GetByCustomerIDAndType(ctx, customerID, entities.TokenType(tokenType)) + if err != nil { + return nil, fmt.Errorf("failed to get updated customer tokens: %w", err) + } + + return mappers.ToCustomerTokensResponse(customerTokens), nil +} + +// DeductTokens deducts tokens from a customer's balance +func (p *CustomerTokensProcessor) DeductTokens(ctx context.Context, customerID uuid.UUID, tokenType string, tokens int64) (*models.CustomerTokensResponse, error) { + if tokens <= 0 { + return nil, errors.New("tokens must be greater than 0") + } + + // Get current customer tokens + customerTokens, err := p.customerTokensRepo.GetByCustomerIDAndType(ctx, customerID, entities.TokenType(tokenType)) + if err != nil { + return nil, fmt.Errorf("customer tokens not found: %w", err) + } + + if customerTokens.Balance < tokens { + return nil, errors.New("insufficient tokens balance") + } + + // Deduct tokens + err = p.customerTokensRepo.DeductTokens(ctx, customerID, entities.TokenType(tokenType), tokens) + if err != nil { + return nil, fmt.Errorf("failed to deduct tokens: %w", err) + } + + // Get updated customer tokens + updatedCustomerTokens, err := p.customerTokensRepo.GetByCustomerIDAndType(ctx, customerID, entities.TokenType(tokenType)) + if err != nil { + return nil, fmt.Errorf("failed to get updated customer tokens: %w", err) + } + + return mappers.ToCustomerTokensResponse(updatedCustomerTokens), nil +} diff --git a/internal/processor/game_play_processor.go b/internal/processor/game_play_processor.go new file mode 100644 index 0000000..194e8b9 --- /dev/null +++ b/internal/processor/game_play_processor.go @@ -0,0 +1,239 @@ +package processor + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" + "context" + "errors" + "fmt" + "math/rand" + "time" + + "github.com/google/uuid" +) + +type GamePlayProcessor struct { + gamePlayRepo *repository.GamePlayRepository + gameRepo *repository.GameRepository + gamePrizeRepo *repository.GamePrizeRepository + customerTokensRepo *repository.CustomerTokensRepository + customerPointsRepo *repository.CustomerPointsRepository +} + +func NewGamePlayProcessor( + gamePlayRepo *repository.GamePlayRepository, + gameRepo *repository.GameRepository, + gamePrizeRepo *repository.GamePrizeRepository, + customerTokensRepo *repository.CustomerTokensRepository, + customerPointsRepo *repository.CustomerPointsRepository, +) *GamePlayProcessor { + return &GamePlayProcessor{ + gamePlayRepo: gamePlayRepo, + gameRepo: gameRepo, + gamePrizeRepo: gamePrizeRepo, + customerTokensRepo: customerTokensRepo, + customerPointsRepo: customerPointsRepo, + } +} + +// CreateGamePlay creates a new game play record +func (p *GamePlayProcessor) CreateGamePlay(ctx context.Context, req *models.CreateGamePlayRequest) (*models.GamePlayResponse, error) { + // Convert request to entity + gamePlay := mappers.ToGamePlayEntity(req) + + // Create game play + err := p.gamePlayRepo.Create(ctx, gamePlay) + if err != nil { + return nil, fmt.Errorf("failed to create game play: %w", err) + } + + return mappers.ToGamePlayResponse(gamePlay), nil +} + +// GetGamePlay retrieves a game play by ID +func (p *GamePlayProcessor) GetGamePlay(ctx context.Context, id uuid.UUID) (*models.GamePlayResponse, error) { + gamePlay, err := p.gamePlayRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("game play not found: %w", err) + } + + return mappers.ToGamePlayResponse(gamePlay), nil +} + +// ListGamePlays retrieves game plays with pagination and filtering +func (p *GamePlayProcessor) ListGamePlays(ctx context.Context, query *models.ListGamePlaysQuery) (*models.PaginatedResponse[models.GamePlayResponse], error) { + // Set default values + if query.Page <= 0 { + query.Page = 1 + } + if query.Limit <= 0 { + query.Limit = 10 + } + if query.Limit > 100 { + query.Limit = 100 + } + + offset := (query.Page - 1) * query.Limit + + // Get game plays from repository + gamePlays, total, err := p.gamePlayRepo.List( + ctx, + offset, + query.Limit, + query.Search, + query.GameID, + query.CustomerID, + query.PrizeID, + query.SortBy, + query.SortOrder, + ) + if err != nil { + return nil, fmt.Errorf("failed to list game plays: %w", err) + } + + // Convert to responses + responses := mappers.ToGamePlayResponses(gamePlays) + + // Calculate pagination info + totalPages := int((total + int64(query.Limit) - 1) / int64(query.Limit)) + + return &models.PaginatedResponse[models.GamePlayResponse]{ + Data: responses, + Pagination: models.Pagination{ + Page: query.Page, + Limit: query.Limit, + Total: total, + TotalPages: totalPages, + }, + }, nil +} + +// PlayGame handles the game playing logic +func (p *GamePlayProcessor) PlayGame(ctx context.Context, req *models.PlayGameRequest) (*models.PlayGameResponse, error) { + // Verify game exists and is active + game, err := p.gameRepo.GetByID(ctx, req.GameID) + if err != nil { + return nil, fmt.Errorf("game not found: %w", err) + } + + if !game.IsActive { + return nil, errors.New("game is not active") + } + + // Convert GameType to TokenType + tokenType := entities.TokenType(game.Type) + + // Check if customer has enough tokens + customerTokens, err := p.customerTokensRepo.GetByCustomerIDAndType(ctx, req.CustomerID, tokenType) + if err != nil { + return nil, fmt.Errorf("customer tokens not found: %w", err) + } + + if customerTokens.Balance < int64(req.TokenUsed) { + return nil, errors.New("insufficient tokens") + } + + // Deduct tokens + err = p.customerTokensRepo.DeductTokens(ctx, req.CustomerID, tokenType, int64(req.TokenUsed)) + if err != nil { + return nil, fmt.Errorf("failed to deduct tokens: %w", err) + } + + // Get available prizes + availablePrizes, err := p.gamePrizeRepo.GetAvailablePrizes(ctx, req.GameID) + if err != nil { + return nil, fmt.Errorf("failed to get available prizes: %w", err) + } + + if len(availablePrizes) == 0 { + return nil, errors.New("no prizes available") + } + + // Convert entities to models for prize selection + prizeResponses := make([]models.GamePrizeResponse, len(availablePrizes)) + for i, prize := range availablePrizes { + prizeResponses[i] = *mappers.ToGamePrizeResponse(&prize) + } + + // Select prize based on weight + selectedPrize := p.selectPrizeByWeight(prizeResponses) + + // Generate random seed for audit + randomSeed := fmt.Sprintf("%d", time.Now().UnixNano()) + + // Create game play record + gamePlay := &models.CreateGamePlayRequest{ + GameID: req.GameID, + CustomerID: req.CustomerID, + TokenUsed: req.TokenUsed, + RandomSeed: &randomSeed, + } + + gamePlayEntity := mappers.ToGamePlayEntity(gamePlay) + if selectedPrize != nil { + gamePlayEntity.PrizeID = &selectedPrize.ID + } + + err = p.gamePlayRepo.Create(ctx, gamePlayEntity) + if err != nil { + // Rollback token deduction + p.customerTokensRepo.AddTokens(ctx, req.CustomerID, tokenType, int64(req.TokenUsed)) + return nil, fmt.Errorf("failed to create game play: %w", err) + } + + // Decrease prize stock if prize was won + if selectedPrize != nil { + err = p.gamePrizeRepo.DecreaseStock(ctx, selectedPrize.ID, 1) + if err != nil { + // Log error but don't fail the transaction + fmt.Printf("Warning: failed to decrease prize stock: %v\n", err) + } + } + + // Get updated token balance + updatedTokens, err := p.customerTokensRepo.GetByCustomerIDAndType(ctx, req.CustomerID, tokenType) + if err != nil { + return nil, fmt.Errorf("failed to get updated token balance: %w", err) + } + + return &models.PlayGameResponse{ + GamePlay: *mappers.ToGamePlayResponse(gamePlayEntity), + PrizeWon: selectedPrize, + TokensRemaining: updatedTokens.Balance, + }, nil +} + +// selectPrizeByWeight selects a prize based on weight distribution +func (p *GamePlayProcessor) selectPrizeByWeight(prizes []models.GamePrizeResponse) *models.GamePrizeResponse { + if len(prizes) == 0 { + return nil + } + + // Calculate total weight + totalWeight := 0 + for _, prize := range prizes { + totalWeight += prize.Weight + } + + if totalWeight == 0 { + return nil + } + + // Generate random number + rand.Seed(time.Now().UnixNano()) + randomNumber := rand.Intn(totalWeight) + + // Select prize based on cumulative weight + currentWeight := 0 + for _, prize := range prizes { + currentWeight += prize.Weight + if randomNumber < currentWeight { + return &prize + } + } + + // Fallback to last prize + return &prizes[len(prizes)-1] +} diff --git a/internal/processor/game_prize_processor.go b/internal/processor/game_prize_processor.go new file mode 100644 index 0000000..6527e3a --- /dev/null +++ b/internal/processor/game_prize_processor.go @@ -0,0 +1,148 @@ +package processor + +import ( + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" + "context" + "fmt" + + "github.com/google/uuid" +) + +type GamePrizeProcessor struct { + gamePrizeRepo *repository.GamePrizeRepository +} + +func NewGamePrizeProcessor(gamePrizeRepo *repository.GamePrizeRepository) *GamePrizeProcessor { + return &GamePrizeProcessor{ + gamePrizeRepo: gamePrizeRepo, + } +} + +// CreateGamePrize creates a new game prize +func (p *GamePrizeProcessor) CreateGamePrize(ctx context.Context, req *models.CreateGamePrizeRequest) (*models.GamePrizeResponse, error) { + // Convert request to entity + gamePrize := mappers.ToGamePrizeEntity(req) + + // Create game prize + err := p.gamePrizeRepo.Create(ctx, gamePrize) + if err != nil { + return nil, fmt.Errorf("failed to create game prize: %w", err) + } + + return mappers.ToGamePrizeResponse(gamePrize), nil +} + +// GetGamePrize retrieves a game prize by ID +func (p *GamePrizeProcessor) GetGamePrize(ctx context.Context, id uuid.UUID) (*models.GamePrizeResponse, error) { + gamePrize, err := p.gamePrizeRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("game prize not found: %w", err) + } + + return mappers.ToGamePrizeResponse(gamePrize), nil +} + +// GetGamePrizesByGameID retrieves all prizes for a specific game +func (p *GamePrizeProcessor) GetGamePrizesByGameID(ctx context.Context, gameID uuid.UUID) ([]models.GamePrizeResponse, error) { + gamePrizes, err := p.gamePrizeRepo.GetByGameID(ctx, gameID) + if err != nil { + return nil, fmt.Errorf("failed to get game prizes: %w", err) + } + + return mappers.ToGamePrizeResponses(gamePrizes), nil +} + +// ListGamePrizes retrieves game prizes with pagination and filtering +func (p *GamePrizeProcessor) ListGamePrizes(ctx context.Context, query *models.ListGamePrizesQuery) (*models.PaginatedResponse[models.GamePrizeResponse], error) { + // Set default values + if query.Page <= 0 { + query.Page = 1 + } + if query.Limit <= 0 { + query.Limit = 10 + } + if query.Limit > 100 { + query.Limit = 100 + } + + offset := (query.Page - 1) * query.Limit + + // Get game prizes from repository + gamePrizes, total, err := p.gamePrizeRepo.List( + ctx, + offset, + query.Limit, + query.Search, + query.GameID, + query.SortBy, + query.SortOrder, + ) + if err != nil { + return nil, fmt.Errorf("failed to list game prizes: %w", err) + } + + // Convert to responses + responses := mappers.ToGamePrizeResponses(gamePrizes) + + // Calculate pagination info + totalPages := int((total + int64(query.Limit) - 1) / int64(query.Limit)) + + return &models.PaginatedResponse[models.GamePrizeResponse]{ + Data: responses, + Pagination: models.Pagination{ + Page: query.Page, + Limit: query.Limit, + Total: total, + TotalPages: totalPages, + }, + }, nil +} + +// UpdateGamePrize updates an existing game prize +func (p *GamePrizeProcessor) UpdateGamePrize(ctx context.Context, id uuid.UUID, req *models.UpdateGamePrizeRequest) (*models.GamePrizeResponse, error) { + // Get existing game prize + gamePrize, err := p.gamePrizeRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("game prize not found: %w", err) + } + + // Update game prize fields + mappers.UpdateGamePrizeEntity(gamePrize, req) + + // Save updated game prize + err = p.gamePrizeRepo.Update(ctx, gamePrize) + if err != nil { + return nil, fmt.Errorf("failed to update game prize: %w", err) + } + + return mappers.ToGamePrizeResponse(gamePrize), nil +} + +// DeleteGamePrize deletes a game prize +func (p *GamePrizeProcessor) DeleteGamePrize(ctx context.Context, id uuid.UUID) error { + // Get existing game prize + _, err := p.gamePrizeRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("game prize not found: %w", err) + } + + // Delete game prize + err = p.gamePrizeRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete game prize: %w", err) + } + + return nil +} + +// GetAvailablePrizes gets all available prizes for a game (with stock > 0) +func (p *GamePrizeProcessor) GetAvailablePrizes(ctx context.Context, gameID uuid.UUID) ([]models.GamePrizeResponse, error) { + gamePrizes, err := p.gamePrizeRepo.GetAvailablePrizes(ctx, gameID) + if err != nil { + return nil, fmt.Errorf("failed to get available prizes: %w", err) + } + + return mappers.ToGamePrizeResponses(gamePrizes), nil +} diff --git a/internal/processor/game_processor.go b/internal/processor/game_processor.go new file mode 100644 index 0000000..6c9dea0 --- /dev/null +++ b/internal/processor/game_processor.go @@ -0,0 +1,139 @@ +package processor + +import ( + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" + "context" + "fmt" + + "github.com/google/uuid" +) + +type GameProcessor struct { + gameRepo *repository.GameRepository +} + +func NewGameProcessor(gameRepo *repository.GameRepository) *GameProcessor { + return &GameProcessor{ + gameRepo: gameRepo, + } +} + +// CreateGame creates a new game +func (p *GameProcessor) CreateGame(ctx context.Context, req *models.CreateGameRequest) (*models.GameResponse, error) { + // Convert request to entity + game := mappers.ToGameEntity(req) + + // Create game + err := p.gameRepo.Create(ctx, game) + if err != nil { + return nil, fmt.Errorf("failed to create game: %w", err) + } + + return mappers.ToGameResponse(game), nil +} + +// GetGame retrieves a game by ID +func (p *GameProcessor) GetGame(ctx context.Context, id uuid.UUID) (*models.GameResponse, error) { + game, err := p.gameRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("game not found: %w", err) + } + + return mappers.ToGameResponse(game), nil +} + +// ListGames retrieves games with pagination and filtering +func (p *GameProcessor) ListGames(ctx context.Context, query *models.ListGamesQuery) (*models.PaginatedResponse[models.GameResponse], error) { + // Set default values + if query.Page <= 0 { + query.Page = 1 + } + if query.Limit <= 0 { + query.Limit = 10 + } + if query.Limit > 100 { + query.Limit = 100 + } + + offset := (query.Page - 1) * query.Limit + + // Get games from repository + games, total, err := p.gameRepo.List( + ctx, + offset, + query.Limit, + query.Search, + query.Type, + query.IsActive, + query.SortBy, + query.SortOrder, + ) + if err != nil { + return nil, fmt.Errorf("failed to list games: %w", err) + } + + // Convert to responses + responses := mappers.ToGameResponses(games) + + // Calculate pagination info + totalPages := int((total + int64(query.Limit) - 1) / int64(query.Limit)) + + return &models.PaginatedResponse[models.GameResponse]{ + Data: responses, + Pagination: models.Pagination{ + Page: query.Page, + Limit: query.Limit, + Total: total, + TotalPages: totalPages, + }, + }, nil +} + +// UpdateGame updates an existing game +func (p *GameProcessor) UpdateGame(ctx context.Context, id uuid.UUID, req *models.UpdateGameRequest) (*models.GameResponse, error) { + // Get existing game + game, err := p.gameRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("game not found: %w", err) + } + + // Update game fields + mappers.UpdateGameEntity(game, req) + + // Save updated game + err = p.gameRepo.Update(ctx, game) + if err != nil { + return nil, fmt.Errorf("failed to update game: %w", err) + } + + return mappers.ToGameResponse(game), nil +} + +// DeleteGame deletes a game +func (p *GameProcessor) DeleteGame(ctx context.Context, id uuid.UUID) error { + // Get existing game + _, err := p.gameRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("game not found: %w", err) + } + + // Delete game + err = p.gameRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete game: %w", err) + } + + return nil +} + +// GetActiveGames gets all active games +func (p *GameProcessor) GetActiveGames(ctx context.Context) ([]models.GameResponse, error) { + games, err := p.gameRepo.GetActiveGames(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get active games: %w", err) + } + + return mappers.ToGameResponses(games), nil +} diff --git a/internal/processor/omset_tracker_processor.go b/internal/processor/omset_tracker_processor.go new file mode 100644 index 0000000..65574a6 --- /dev/null +++ b/internal/processor/omset_tracker_processor.go @@ -0,0 +1,163 @@ +package processor + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" + "context" + "fmt" + "time" + + "github.com/google/uuid" +) + +type OmsetTrackerProcessor struct { + omsetTrackerRepo *repository.OmsetTrackerRepository +} + +func NewOmsetTrackerProcessor(omsetTrackerRepo *repository.OmsetTrackerRepository) *OmsetTrackerProcessor { + return &OmsetTrackerProcessor{ + omsetTrackerRepo: omsetTrackerRepo, + } +} + +// CreateOmsetTracker creates a new omset tracker record +func (p *OmsetTrackerProcessor) CreateOmsetTracker(ctx context.Context, req *models.CreateOmsetTrackerRequest) (*models.OmsetTrackerResponse, error) { + // Convert request to entity + omsetTracker := mappers.ToOmsetTrackerEntity(req) + + // Create omset tracker + err := p.omsetTrackerRepo.Create(ctx, omsetTracker) + if err != nil { + return nil, fmt.Errorf("failed to create omset tracker: %w", err) + } + + return mappers.ToOmsetTrackerResponse(omsetTracker), nil +} + +// GetOmsetTracker retrieves an omset tracker by ID +func (p *OmsetTrackerProcessor) GetOmsetTracker(ctx context.Context, id uuid.UUID) (*models.OmsetTrackerResponse, error) { + omsetTracker, err := p.omsetTrackerRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("omset tracker not found: %w", err) + } + + return mappers.ToOmsetTrackerResponse(omsetTracker), nil +} + +// ListOmsetTrackers retrieves omset trackers with pagination and filtering +func (p *OmsetTrackerProcessor) ListOmsetTrackers(ctx context.Context, query *models.ListOmsetTrackerQuery) (*models.PaginatedResponse[models.OmsetTrackerResponse], error) { + // Set default values + if query.Page <= 0 { + query.Page = 1 + } + if query.Limit <= 0 { + query.Limit = 10 + } + if query.Limit > 100 { + query.Limit = 100 + } + + offset := (query.Page - 1) * query.Limit + + // Get omset trackers from repository + omsetTrackers, total, err := p.omsetTrackerRepo.List( + ctx, + offset, + query.Limit, + query.Search, + query.PeriodType, + query.GameID, + query.From, + query.To, + query.SortBy, + query.SortOrder, + ) + if err != nil { + return nil, fmt.Errorf("failed to list omset trackers: %w", err) + } + + // Convert to responses + responses := mappers.ToOmsetTrackerResponses(omsetTrackers) + + // Calculate pagination info + totalPages := int((total + int64(query.Limit) - 1) / int64(query.Limit)) + + return &models.PaginatedResponse[models.OmsetTrackerResponse]{ + Data: responses, + Pagination: models.Pagination{ + Page: query.Page, + Limit: query.Limit, + Total: total, + TotalPages: totalPages, + }, + }, nil +} + +// UpdateOmsetTracker updates an existing omset tracker +func (p *OmsetTrackerProcessor) UpdateOmsetTracker(ctx context.Context, id uuid.UUID, req *models.UpdateOmsetTrackerRequest) (*models.OmsetTrackerResponse, error) { + // Get existing omset tracker + omsetTracker, err := p.omsetTrackerRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("omset tracker not found: %w", err) + } + + // Update omset tracker fields + mappers.UpdateOmsetTrackerEntity(omsetTracker, req) + + // Save updated omset tracker + err = p.omsetTrackerRepo.Update(ctx, omsetTracker) + if err != nil { + return nil, fmt.Errorf("failed to update omset tracker: %w", err) + } + + return mappers.ToOmsetTrackerResponse(omsetTracker), nil +} + +// DeleteOmsetTracker deletes an omset tracker +func (p *OmsetTrackerProcessor) DeleteOmsetTracker(ctx context.Context, id uuid.UUID) error { + // Get existing omset tracker + _, err := p.omsetTrackerRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("omset tracker not found: %w", err) + } + + // Delete omset tracker + err = p.omsetTrackerRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete omset tracker: %w", err) + } + + return nil +} + +// AddOmset adds omset to a specific period +func (p *OmsetTrackerProcessor) AddOmset(ctx context.Context, periodType string, periodStart, periodEnd time.Time, amount int64, gameID *uuid.UUID) (*models.OmsetTrackerResponse, error) { + if amount <= 0 { + return nil, fmt.Errorf("amount must be greater than 0") + } + + // Convert string to PeriodType + periodTypeEnum := entities.PeriodType(periodType) + + // Get or create period tracker + omsetTracker, err := p.omsetTrackerRepo.GetOrCreatePeriod(ctx, periodTypeEnum, periodStart, periodEnd, gameID) + if err != nil { + return nil, fmt.Errorf("failed to get or create period tracker: %w", err) + } + + // Add omset + err = p.omsetTrackerRepo.AddOmset(ctx, periodTypeEnum, periodStart, periodEnd, amount, gameID) + if err != nil { + return nil, fmt.Errorf("failed to add omset: %w", err) + } + + // Get updated tracker + updatedTracker, err := p.omsetTrackerRepo.GetByID(ctx, omsetTracker.ID) + if err != nil { + return nil, fmt.Errorf("failed to get updated tracker: %w", err) + } + + return mappers.ToOmsetTrackerResponse(updatedTracker), nil +} diff --git a/internal/processor/tier_processor.go b/internal/processor/tier_processor.go new file mode 100644 index 0000000..4832c93 --- /dev/null +++ b/internal/processor/tier_processor.go @@ -0,0 +1,137 @@ +package processor + +import ( + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" + "context" + "fmt" + + "github.com/google/uuid" +) + +type TierProcessor struct { + tierRepo *repository.TierRepository +} + +func NewTierProcessor(tierRepo *repository.TierRepository) *TierProcessor { + return &TierProcessor{ + tierRepo: tierRepo, + } +} + +// CreateTier creates a new tier +func (p *TierProcessor) CreateTier(ctx context.Context, req *models.CreateTierRequest) (*models.TierResponse, error) { + // Convert request to entity + tier := mappers.ToTierEntity(req) + + // Create tier + err := p.tierRepo.Create(ctx, tier) + if err != nil { + return nil, fmt.Errorf("failed to create tier: %w", err) + } + + return mappers.ToTierResponse(tier), nil +} + +// GetTier retrieves a tier by ID +func (p *TierProcessor) GetTier(ctx context.Context, id uuid.UUID) (*models.TierResponse, error) { + tier, err := p.tierRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("tier not found: %w", err) + } + + return mappers.ToTierResponse(tier), nil +} + +// ListTiers retrieves tiers with pagination and filtering +func (p *TierProcessor) ListTiers(ctx context.Context, query *models.ListTiersQuery) (*models.PaginatedResponse[models.TierResponse], error) { + // Set default values + if query.Page <= 0 { + query.Page = 1 + } + if query.Limit <= 0 { + query.Limit = 10 + } + if query.Limit > 100 { + query.Limit = 100 + } + + offset := (query.Page - 1) * query.Limit + + // Get tiers from repository + tiers, total, err := p.tierRepo.List( + ctx, + offset, + query.Limit, + query.Search, + query.SortBy, + query.SortOrder, + ) + if err != nil { + return nil, fmt.Errorf("failed to list tiers: %w", err) + } + + // Convert to responses + responses := mappers.ToTierResponses(tiers) + + // Calculate pagination info + totalPages := int((total + int64(query.Limit) - 1) / int64(query.Limit)) + + return &models.PaginatedResponse[models.TierResponse]{ + Data: responses, + Pagination: models.Pagination{ + Page: query.Page, + Limit: query.Limit, + Total: total, + TotalPages: totalPages, + }, + }, nil +} + +// UpdateTier updates an existing tier +func (p *TierProcessor) UpdateTier(ctx context.Context, id uuid.UUID, req *models.UpdateTierRequest) (*models.TierResponse, error) { + // Get existing tier + tier, err := p.tierRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("tier not found: %w", err) + } + + // Update tier fields + mappers.UpdateTierEntity(tier, req) + + // Save updated tier + err = p.tierRepo.Update(ctx, tier) + if err != nil { + return nil, fmt.Errorf("failed to update tier: %w", err) + } + + return mappers.ToTierResponse(tier), nil +} + +// DeleteTier deletes a tier +func (p *TierProcessor) DeleteTier(ctx context.Context, id uuid.UUID) error { + // Get existing tier + _, err := p.tierRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("tier not found: %w", err) + } + + // Delete tier + err = p.tierRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete tier: %w", err) + } + + return nil +} + +// GetTierByPoints gets the appropriate tier for a given point amount +func (p *TierProcessor) GetTierByPoints(ctx context.Context, points int64) (*models.TierResponse, error) { + tier, err := p.tierRepo.GetTierByPoints(ctx, points) + if err != nil { + return nil, fmt.Errorf("tier not found for points: %w", err) + } + + return mappers.ToTierResponse(tier), nil +} diff --git a/internal/repository/customer_points_repository.go b/internal/repository/customer_points_repository.go new file mode 100644 index 0000000..b9714c6 --- /dev/null +++ b/internal/repository/customer_points_repository.go @@ -0,0 +1,117 @@ +package repository + +import ( + "apskel-pos-be/internal/entities" + "context" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type CustomerPointsRepository struct { + db *gorm.DB +} + +func NewCustomerPointsRepository(db *gorm.DB) *CustomerPointsRepository { + return &CustomerPointsRepository{db: db} +} + +func (r *CustomerPointsRepository) Create(ctx context.Context, customerPoints *entities.CustomerPoints) error { + return r.db.WithContext(ctx).Create(customerPoints).Error +} + +func (r *CustomerPointsRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.CustomerPoints, error) { + var customerPoints entities.CustomerPoints + err := r.db.WithContext(ctx).Preload("Customer").Where("id = ?", id).First(&customerPoints).Error + if err != nil { + return nil, err + } + return &customerPoints, nil +} + +func (r *CustomerPointsRepository) GetByCustomerID(ctx context.Context, customerID uuid.UUID) (*entities.CustomerPoints, error) { + var customerPoints entities.CustomerPoints + err := r.db.WithContext(ctx).Preload("Customer").Where("customer_id = ?", customerID).First(&customerPoints).Error + if err != nil { + return nil, err + } + return &customerPoints, nil +} + +func (r *CustomerPointsRepository) List(ctx context.Context, offset, limit int, search string, sortBy, sortOrder string) ([]entities.CustomerPoints, int64, error) { + var customerPoints []entities.CustomerPoints + var total int64 + + query := r.db.WithContext(ctx).Preload("Customer") + + if search != "" { + searchTerm := "%" + search + "%" + query = query.Joins("JOIN customers ON customer_points.customer_id = customers.id"). + Where("customers.name ILIKE ? OR customers.email ILIKE ?", searchTerm, searchTerm) + } + + if err := query.Model(&entities.CustomerPoints{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + if sortBy != "" { + if sortOrder == "" { + sortOrder = "asc" + } + query = query.Order(fmt.Sprintf("customer_points.%s %s", sortBy, sortOrder)) + } else { + query = query.Order("customer_points.created_at DESC") + } + + err := query.Offset(offset).Limit(limit).Find(&customerPoints).Error + if err != nil { + return nil, 0, err + } + + return customerPoints, total, nil +} + +func (r *CustomerPointsRepository) Update(ctx context.Context, customerPoints *entities.CustomerPoints) error { + return r.db.WithContext(ctx).Save(customerPoints).Error +} + +func (r *CustomerPointsRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.CustomerPoints{}, id).Error +} + +func (r *CustomerPointsRepository) AddPoints(ctx context.Context, customerID uuid.UUID, points int64) error { + return r.db.WithContext(ctx).Model(&entities.CustomerPoints{}). + Where("customer_id = ?", customerID). + Update("balance", gorm.Expr("balance + ?", points)).Error +} + +func (r *CustomerPointsRepository) DeductPoints(ctx context.Context, customerID uuid.UUID, points int64) error { + return r.db.WithContext(ctx).Model(&entities.CustomerPoints{}). + Where("customer_id = ? AND balance >= ?", customerID, points). + Update("balance", gorm.Expr("balance - ?", points)).Error +} + +func (r *CustomerPointsRepository) EnsureCustomerPoints(ctx context.Context, customerID uuid.UUID) (*entities.CustomerPoints, error) { + customerPoints, err := r.GetByCustomerID(ctx, customerID) + if err == nil { + return customerPoints, nil + } + + if err != gorm.ErrRecordNotFound { + return nil, err + } + + // Create new customer points record + newCustomerPoints := &entities.CustomerPoints{ + CustomerID: customerID, + Balance: 0, + } + + err = r.Create(ctx, newCustomerPoints) + if err != nil { + return nil, err + } + + return newCustomerPoints, nil +} diff --git a/internal/repository/customer_tokens_repository.go b/internal/repository/customer_tokens_repository.go new file mode 100644 index 0000000..de29903 --- /dev/null +++ b/internal/repository/customer_tokens_repository.go @@ -0,0 +1,131 @@ +package repository + +import ( + "apskel-pos-be/internal/entities" + "context" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type CustomerTokensRepository struct { + db *gorm.DB +} + +func NewCustomerTokensRepository(db *gorm.DB) *CustomerTokensRepository { + return &CustomerTokensRepository{db: db} +} + +func (r *CustomerTokensRepository) Create(ctx context.Context, customerTokens *entities.CustomerTokens) error { + return r.db.WithContext(ctx).Create(customerTokens).Error +} + +func (r *CustomerTokensRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.CustomerTokens, error) { + var customerTokens entities.CustomerTokens + err := r.db.WithContext(ctx).Preload("Customer").Where("id = ?", id).First(&customerTokens).Error + if err != nil { + return nil, err + } + return &customerTokens, nil +} + +func (r *CustomerTokensRepository) GetByCustomerIDAndType(ctx context.Context, customerID uuid.UUID, tokenType entities.TokenType) (*entities.CustomerTokens, error) { + var customerTokens entities.CustomerTokens + err := r.db.WithContext(ctx).Preload("Customer").Where("customer_id = ? AND token_type = ?", customerID, tokenType).First(&customerTokens).Error + if err != nil { + return nil, err + } + return &customerTokens, nil +} + +func (r *CustomerTokensRepository) GetByCustomerID(ctx context.Context, customerID uuid.UUID) ([]entities.CustomerTokens, error) { + var customerTokens []entities.CustomerTokens + err := r.db.WithContext(ctx).Preload("Customer").Where("customer_id = ?", customerID).Find(&customerTokens).Error + if err != nil { + return nil, err + } + return customerTokens, nil +} + +func (r *CustomerTokensRepository) List(ctx context.Context, offset, limit int, search, tokenType string, sortBy, sortOrder string) ([]entities.CustomerTokens, int64, error) { + var customerTokens []entities.CustomerTokens + var total int64 + + query := r.db.WithContext(ctx).Preload("Customer") + + if search != "" { + searchTerm := "%" + search + "%" + query = query.Joins("JOIN customers ON customer_tokens.customer_id = customers.id"). + Where("customers.name ILIKE ? OR customers.email ILIKE ?", searchTerm, searchTerm) + } + + if tokenType != "" { + query = query.Where("token_type = ?", tokenType) + } + + if err := query.Model(&entities.CustomerTokens{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + if sortBy != "" { + if sortOrder == "" { + sortOrder = "asc" + } + query = query.Order(fmt.Sprintf("customer_tokens.%s %s", sortBy, sortOrder)) + } else { + query = query.Order("customer_tokens.created_at DESC") + } + + err := query.Offset(offset).Limit(limit).Find(&customerTokens).Error + if err != nil { + return nil, 0, err + } + + return customerTokens, total, nil +} + +func (r *CustomerTokensRepository) Update(ctx context.Context, customerTokens *entities.CustomerTokens) error { + return r.db.WithContext(ctx).Save(customerTokens).Error +} + +func (r *CustomerTokensRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.CustomerTokens{}, id).Error +} + +func (r *CustomerTokensRepository) AddTokens(ctx context.Context, customerID uuid.UUID, tokenType entities.TokenType, tokens int64) error { + return r.db.WithContext(ctx).Model(&entities.CustomerTokens{}). + Where("customer_id = ? AND token_type = ?", customerID, tokenType). + Update("balance", gorm.Expr("balance + ?", tokens)).Error +} + +func (r *CustomerTokensRepository) DeductTokens(ctx context.Context, customerID uuid.UUID, tokenType entities.TokenType, tokens int64) error { + return r.db.WithContext(ctx).Model(&entities.CustomerTokens{}). + Where("customer_id = ? AND token_type = ? AND balance >= ?", customerID, tokenType, tokens). + Update("balance", gorm.Expr("balance - ?", tokens)).Error +} + +func (r *CustomerTokensRepository) EnsureCustomerTokens(ctx context.Context, customerID uuid.UUID, tokenType entities.TokenType) (*entities.CustomerTokens, error) { + customerTokens, err := r.GetByCustomerIDAndType(ctx, customerID, tokenType) + if err == nil { + return customerTokens, nil + } + + if err != gorm.ErrRecordNotFound { + return nil, err + } + + // Create new customer tokens record + newCustomerTokens := &entities.CustomerTokens{ + CustomerID: customerID, + TokenType: tokenType, + Balance: 0, + } + + err = r.Create(ctx, newCustomerTokens) + if err != nil { + return nil, err + } + + return newCustomerTokens, nil +} diff --git a/internal/repository/game_play_repository.go b/internal/repository/game_play_repository.go new file mode 100644 index 0000000..a35c25d --- /dev/null +++ b/internal/repository/game_play_repository.go @@ -0,0 +1,111 @@ +package repository + +import ( + "apskel-pos-be/internal/entities" + "context" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type GamePlayRepository struct { + db *gorm.DB +} + +func NewGamePlayRepository(db *gorm.DB) *GamePlayRepository { + return &GamePlayRepository{db: db} +} + +func (r *GamePlayRepository) Create(ctx context.Context, gamePlay *entities.GamePlay) error { + return r.db.WithContext(ctx).Create(gamePlay).Error +} + +func (r *GamePlayRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.GamePlay, error) { + var gamePlay entities.GamePlay + err := r.db.WithContext(ctx).Preload("Game").Preload("Customer").Preload("Prize").Where("id = ?", id).First(&gamePlay).Error + if err != nil { + return nil, err + } + return &gamePlay, nil +} + +func (r *GamePlayRepository) List(ctx context.Context, offset, limit int, search string, gameID, customerID, prizeID *uuid.UUID, sortBy, sortOrder string) ([]entities.GamePlay, int64, error) { + var gamePlays []entities.GamePlay + var total int64 + + query := r.db.WithContext(ctx).Preload("Game").Preload("Customer").Preload("Prize") + + if search != "" { + searchTerm := "%" + search + "%" + query = query.Joins("JOIN customers ON game_plays.customer_id = customers.id"). + Where("customers.name ILIKE ? OR customers.email ILIKE ?", searchTerm, searchTerm) + } + + if gameID != nil { + query = query.Where("game_id = ?", *gameID) + } + + if customerID != nil { + query = query.Where("customer_id = ?", *customerID) + } + + if prizeID != nil { + query = query.Where("prize_id = ?", *prizeID) + } + + if err := query.Model(&entities.GamePlay{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + if sortBy != "" { + if sortOrder == "" { + sortOrder = "asc" + } + query = query.Order(fmt.Sprintf("game_plays.%s %s", sortBy, sortOrder)) + } else { + query = query.Order("game_plays.created_at DESC") + } + + err := query.Offset(offset).Limit(limit).Find(&gamePlays).Error + if err != nil { + return nil, 0, err + } + + return gamePlays, total, nil +} + +func (r *GamePlayRepository) Update(ctx context.Context, gamePlay *entities.GamePlay) error { + return r.db.WithContext(ctx).Save(gamePlay).Error +} + +func (r *GamePlayRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.GamePlay{}, id).Error +} + +func (r *GamePlayRepository) GetCustomerPlays(ctx context.Context, customerID uuid.UUID, limit int) ([]entities.GamePlay, error) { + var gamePlays []entities.GamePlay + err := r.db.WithContext(ctx).Preload("Game").Preload("Prize"). + Where("customer_id = ?", customerID). + Order("created_at DESC"). + Limit(limit). + Find(&gamePlays).Error + if err != nil { + return nil, err + } + return gamePlays, nil +} + +func (r *GamePlayRepository) GetGameStats(ctx context.Context, gameID uuid.UUID) (map[string]interface{}, error) { + var stats map[string]interface{} + + err := r.db.WithContext(ctx).Model(&entities.GamePlay{}). + Select("COUNT(*) as total_plays, SUM(token_used) as total_tokens_used"). + Where("game_id = ?", gameID). + Scan(&stats).Error + if err != nil { + return nil, err + } + + return stats, nil +} diff --git a/internal/repository/game_prize_repository.go b/internal/repository/game_prize_repository.go new file mode 100644 index 0000000..243bf73 --- /dev/null +++ b/internal/repository/game_prize_repository.go @@ -0,0 +1,102 @@ +package repository + +import ( + "apskel-pos-be/internal/entities" + "context" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type GamePrizeRepository struct { + db *gorm.DB +} + +func NewGamePrizeRepository(db *gorm.DB) *GamePrizeRepository { + return &GamePrizeRepository{db: db} +} + +func (r *GamePrizeRepository) Create(ctx context.Context, gamePrize *entities.GamePrize) error { + return r.db.WithContext(ctx).Create(gamePrize).Error +} + +func (r *GamePrizeRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.GamePrize, error) { + var gamePrize entities.GamePrize + err := r.db.WithContext(ctx).Preload("Game").Preload("FallbackPrize").Where("id = ?", id).First(&gamePrize).Error + if err != nil { + return nil, err + } + return &gamePrize, nil +} + +func (r *GamePrizeRepository) GetByGameID(ctx context.Context, gameID uuid.UUID) ([]entities.GamePrize, error) { + var gamePrizes []entities.GamePrize + err := r.db.WithContext(ctx).Preload("Game").Preload("FallbackPrize").Where("game_id = ?", gameID).Find(&gamePrizes).Error + if err != nil { + return nil, err + } + return gamePrizes, nil +} + +func (r *GamePrizeRepository) List(ctx context.Context, offset, limit int, search string, gameID *uuid.UUID, sortBy, sortOrder string) ([]entities.GamePrize, int64, error) { + var gamePrizes []entities.GamePrize + var total int64 + + query := r.db.WithContext(ctx).Preload("Game").Preload("FallbackPrize") + + if search != "" { + searchTerm := "%" + search + "%" + query = query.Where("name ILIKE ?", searchTerm) + } + + if gameID != nil { + query = query.Where("game_id = ?", *gameID) + } + + if err := query.Model(&entities.GamePrize{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + if sortBy != "" { + if sortOrder == "" { + sortOrder = "asc" + } + query = query.Order(fmt.Sprintf("game_prizes.%s %s", sortBy, sortOrder)) + } else { + query = query.Order("game_prizes.weight DESC") + } + + err := query.Offset(offset).Limit(limit).Find(&gamePrizes).Error + if err != nil { + return nil, 0, err + } + + return gamePrizes, total, nil +} + +func (r *GamePrizeRepository) Update(ctx context.Context, gamePrize *entities.GamePrize) error { + return r.db.WithContext(ctx).Save(gamePrize).Error +} + +func (r *GamePrizeRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.GamePrize{}, id).Error +} + +func (r *GamePrizeRepository) DecreaseStock(ctx context.Context, id uuid.UUID, amount int) error { + return r.db.WithContext(ctx).Model(&entities.GamePrize{}). + Where("id = ? AND stock >= ?", id, amount). + Update("stock", gorm.Expr("stock - ?", amount)).Error +} + +func (r *GamePrizeRepository) GetAvailablePrizes(ctx context.Context, gameID uuid.UUID) ([]entities.GamePrize, error) { + var gamePrizes []entities.GamePrize + err := r.db.WithContext(ctx).Preload("Game").Preload("FallbackPrize"). + Where("game_id = ? AND stock > 0", gameID). + Order("weight DESC"). + Find(&gamePrizes).Error + if err != nil { + return nil, err + } + return gamePrizes, nil +} diff --git a/internal/repository/game_repository.go b/internal/repository/game_repository.go new file mode 100644 index 0000000..2db5a98 --- /dev/null +++ b/internal/repository/game_repository.go @@ -0,0 +1,88 @@ +package repository + +import ( + "apskel-pos-be/internal/entities" + "context" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type GameRepository struct { + db *gorm.DB +} + +func NewGameRepository(db *gorm.DB) *GameRepository { + return &GameRepository{db: db} +} + +func (r *GameRepository) Create(ctx context.Context, game *entities.Game) error { + return r.db.WithContext(ctx).Create(game).Error +} + +func (r *GameRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.Game, error) { + var game entities.Game + err := r.db.WithContext(ctx).Preload("Prizes").Where("id = ?", id).First(&game).Error + if err != nil { + return nil, err + } + return &game, nil +} + +func (r *GameRepository) List(ctx context.Context, offset, limit int, search, gameType string, isActive *bool, sortBy, sortOrder string) ([]entities.Game, int64, error) { + var games []entities.Game + var total int64 + + query := r.db.WithContext(ctx).Preload("Prizes") + + if search != "" { + searchTerm := "%" + search + "%" + query = query.Where("name ILIKE ?", searchTerm) + } + + if gameType != "" { + query = query.Where("type = ?", gameType) + } + + if isActive != nil { + query = query.Where("is_active = ?", *isActive) + } + + if err := query.Model(&entities.Game{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + if sortBy != "" { + if sortOrder == "" { + sortOrder = "asc" + } + query = query.Order(fmt.Sprintf("%s %s", sortBy, sortOrder)) + } else { + query = query.Order("created_at DESC") + } + + err := query.Offset(offset).Limit(limit).Find(&games).Error + if err != nil { + return nil, 0, err + } + + return games, total, nil +} + +func (r *GameRepository) Update(ctx context.Context, game *entities.Game) error { + return r.db.WithContext(ctx).Save(game).Error +} + +func (r *GameRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Game{}, id).Error +} + +func (r *GameRepository) GetActiveGames(ctx context.Context) ([]entities.Game, error) { + var games []entities.Game + err := r.db.WithContext(ctx).Preload("Prizes").Where("is_active = ?", true).Find(&games).Error + if err != nil { + return nil, err + } + return games, nil +} diff --git a/internal/repository/omset_tracker_repository.go b/internal/repository/omset_tracker_repository.go new file mode 100644 index 0000000..93dd79e --- /dev/null +++ b/internal/repository/omset_tracker_repository.go @@ -0,0 +1,150 @@ +package repository + +import ( + "apskel-pos-be/internal/entities" + "context" + "fmt" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type OmsetTrackerRepository struct { + db *gorm.DB +} + +func NewOmsetTrackerRepository(db *gorm.DB) *OmsetTrackerRepository { + return &OmsetTrackerRepository{db: db} +} + +func (r *OmsetTrackerRepository) Create(ctx context.Context, omsetTracker *entities.OmsetTracker) error { + return r.db.WithContext(ctx).Create(omsetTracker).Error +} + +func (r *OmsetTrackerRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.OmsetTracker, error) { + var omsetTracker entities.OmsetTracker + err := r.db.WithContext(ctx).Preload("Game").Where("id = ?", id).First(&omsetTracker).Error + if err != nil { + return nil, err + } + return &omsetTracker, nil +} + +func (r *OmsetTrackerRepository) List(ctx context.Context, offset, limit int, search, periodType string, gameID *uuid.UUID, from, to *time.Time, sortBy, sortOrder string) ([]entities.OmsetTracker, int64, error) { + var omsetTrackers []entities.OmsetTracker + var total int64 + + query := r.db.WithContext(ctx).Preload("Game") + + if search != "" { + searchTerm := "%" + search + "%" + query = query.Joins("LEFT JOIN games ON omset_tracker.game_id = games.id"). + Where("games.name ILIKE ?", searchTerm) + } + + if periodType != "" { + query = query.Where("period_type = ?", periodType) + } + + if gameID != nil { + query = query.Where("game_id = ?", *gameID) + } + + if from != nil { + query = query.Where("period_start >= ?", *from) + } + + if to != nil { + query = query.Where("period_end <= ?", *to) + } + + if err := query.Model(&entities.OmsetTracker{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + if sortBy != "" { + if sortOrder == "" { + sortOrder = "asc" + } + query = query.Order(fmt.Sprintf("omset_tracker.%s %s", sortBy, sortOrder)) + } else { + query = query.Order("omset_tracker.period_start DESC") + } + + err := query.Offset(offset).Limit(limit).Find(&omsetTrackers).Error + if err != nil { + return nil, 0, err + } + + return omsetTrackers, total, nil +} + +func (r *OmsetTrackerRepository) Update(ctx context.Context, omsetTracker *entities.OmsetTracker) error { + return r.db.WithContext(ctx).Save(omsetTracker).Error +} + +func (r *OmsetTrackerRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.OmsetTracker{}, id).Error +} + +func (r *OmsetTrackerRepository) AddOmset(ctx context.Context, periodType entities.PeriodType, periodStart, periodEnd time.Time, amount int64, gameID *uuid.UUID) error { + return r.db.WithContext(ctx).Model(&entities.OmsetTracker{}). + Where("period_type = ? AND period_start = ? AND period_end = ? AND game_id = ?", periodType, periodStart, periodEnd, gameID). + Update("total", gorm.Expr("total + ?", amount)).Error +} + +func (r *OmsetTrackerRepository) GetOrCreatePeriod(ctx context.Context, periodType entities.PeriodType, periodStart, periodEnd time.Time, gameID *uuid.UUID) (*entities.OmsetTracker, error) { + var omsetTracker entities.OmsetTracker + + err := r.db.WithContext(ctx).Preload("Game"). + Where("period_type = ? AND period_start = ? AND period_end = ? AND game_id = ?", periodType, periodStart, periodEnd, gameID). + First(&omsetTracker).Error + + if err == nil { + return &omsetTracker, nil + } + + if err != gorm.ErrRecordNotFound { + return nil, err + } + + // Create new period + newOmsetTracker := &entities.OmsetTracker{ + PeriodType: periodType, + PeriodStart: periodStart, + PeriodEnd: periodEnd, + Total: 0, + GameID: gameID, + } + + err = r.Create(ctx, newOmsetTracker) + if err != nil { + return nil, err + } + + return newOmsetTracker, nil +} + +func (r *OmsetTrackerRepository) GetPeriodSummary(ctx context.Context, periodType entities.PeriodType, from, to time.Time) (map[string]interface{}, error) { + var summary map[string]interface{} + + query := r.db.WithContext(ctx).Model(&entities.OmsetTracker{}). + Select("SUM(total) as total_omset, COUNT(*) as period_count"). + Where("period_type = ?", periodType) + + if !from.IsZero() { + query = query.Where("period_start >= ?", from) + } + + if !to.IsZero() { + query = query.Where("period_end <= ?", to) + } + + err := query.Scan(&summary).Error + if err != nil { + return nil, err + } + + return summary, nil +} diff --git a/internal/repository/tier_repository.go b/internal/repository/tier_repository.go new file mode 100644 index 0000000..f632ad6 --- /dev/null +++ b/internal/repository/tier_repository.go @@ -0,0 +1,89 @@ +package repository + +import ( + "apskel-pos-be/internal/entities" + "context" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type TierRepository struct { + db *gorm.DB +} + +func NewTierRepository(db *gorm.DB) *TierRepository { + return &TierRepository{db: db} +} + +func (r *TierRepository) Create(ctx context.Context, tier *entities.Tier) error { + return r.db.WithContext(ctx).Create(tier).Error +} + +func (r *TierRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.Tier, error) { + var tier entities.Tier + err := r.db.WithContext(ctx).Where("id = ?", id).First(&tier).Error + if err != nil { + return nil, err + } + return &tier, nil +} + +func (r *TierRepository) GetByName(ctx context.Context, name string) (*entities.Tier, error) { + var tier entities.Tier + err := r.db.WithContext(ctx).Where("name = ?", name).First(&tier).Error + if err != nil { + return nil, err + } + return &tier, nil +} + +func (r *TierRepository) List(ctx context.Context, offset, limit int, search string, sortBy, sortOrder string) ([]entities.Tier, int64, error) { + var tiers []entities.Tier + var total int64 + + query := r.db.WithContext(ctx) + + if search != "" { + searchTerm := "%" + search + "%" + query = query.Where("name ILIKE ?", searchTerm) + } + + if err := query.Model(&entities.Tier{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + if sortBy != "" { + if sortOrder == "" { + sortOrder = "asc" + } + query = query.Order(fmt.Sprintf("%s %s", sortBy, sortOrder)) + } else { + query = query.Order("min_points ASC") + } + + err := query.Offset(offset).Limit(limit).Find(&tiers).Error + if err != nil { + return nil, 0, err + } + + return tiers, total, nil +} + +func (r *TierRepository) Update(ctx context.Context, tier *entities.Tier) error { + return r.db.WithContext(ctx).Save(tier).Error +} + +func (r *TierRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Tier{}, id).Error +} + +func (r *TierRepository) GetTierByPoints(ctx context.Context, points int64) (*entities.Tier, error) { + var tier entities.Tier + err := r.db.WithContext(ctx).Where("min_points <= ?", points).Order("min_points DESC").First(&tier).Error + if err != nil { + return nil, err + } + return &tier, nil +} diff --git a/internal/router/router.go b/internal/router/router.go index 17a67c1..cd0c757 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -40,6 +40,7 @@ type Router struct { chartOfAccountHandler *handler.ChartOfAccountHandler accountHandler *handler.AccountHandler orderIngredientTransactionHandler *handler.OrderIngredientTransactionHandler + gamificationHandler *handler.GamificationHandler authMiddleware *middleware.AuthMiddleware } @@ -90,7 +91,9 @@ func NewRouter(cfg *config.Config, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, - orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator) *Router { + orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, + gamificationService service.GamificationService, + gamificationValidator validator.GamificationValidator) *Router { return &Router{ config: cfg, @@ -120,6 +123,7 @@ func NewRouter(cfg *config.Config, chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator), accountHandler: handler.NewAccountHandler(accountService, accountValidator), orderIngredientTransactionHandler: handler.NewOrderIngredientTransactionHandler(&orderIngredientTransactionService, orderIngredientTransactionValidator), + gamificationHandler: handler.NewGamificationHandler(gamificationService, gamificationValidator), authMiddleware: authMiddleware, } } @@ -426,6 +430,88 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { orderIngredientTransactions.POST("/bulk", r.orderIngredientTransactionHandler.BulkCreateOrderIngredientTransactions) } + gamification := protected.Group("/marketing") + gamification.Use(r.authMiddleware.RequireAdminOrManager()) + { + //customerPoints := gamification.Group("/customer-points") + //{ + // customerPoints.POST("", r.gamificationHandler.CreateCustomerPoints) + // customerPoints.GET("", r.gamificationHandler.ListCustomerPoints) + // customerPoints.GET("/:id", r.gamificationHandler.GetCustomerPoints) + // customerPoints.PUT("/:id", r.gamificationHandler.UpdateCustomerPoints) + // customerPoints.DELETE("/:id", r.gamificationHandler.DeleteCustomerPoints) + // customerPoints.GET("/customer/:customer_id", r.gamificationHandler.GetCustomerPointsByCustomerID) + // customerPoints.POST("/customer/:customer_id/add", r.gamificationHandler.AddCustomerPoints) + // customerPoints.POST("/customer/:customer_id/deduct", r.gamificationHandler.DeductCustomerPoints) + //} + + // Customer Tokens + //customerTokens := gamification.Group("/customer-tokens") + //{ + // customerTokens.POST("", r.gamificationHandler.CreateCustomerTokens) + // customerTokens.GET("", r.gamificationHandler.ListCustomerTokens) + // customerTokens.GET("/:id", r.gamificationHandler.GetCustomerTokens) + // customerTokens.PUT("/:id", r.gamificationHandler.UpdateCustomerTokens) + // customerTokens.DELETE("/:id", r.gamificationHandler.DeleteCustomerTokens) + // customerTokens.GET("/customer/:customer_id/type/:token_type", r.gamificationHandler.GetCustomerTokensByCustomerIDAndType) + // customerTokens.POST("/customer/:customer_id/type/:token_type/add", r.gamificationHandler.AddCustomerTokens) + // customerTokens.POST("/customer/:customer_id/type/:token_type/deduct", r.gamificationHandler.DeductCustomerTokens) + //} + + // Tiers + tiers := gamification.Group("/tiers") + { + tiers.POST("", r.gamificationHandler.CreateTier) + tiers.GET("", r.gamificationHandler.ListTiers) + tiers.GET("/:id", r.gamificationHandler.GetTier) + tiers.PUT("/:id", r.gamificationHandler.UpdateTier) + tiers.DELETE("/:id", r.gamificationHandler.DeleteTier) + tiers.GET("/by-points/:points", r.gamificationHandler.GetTierByPoints) + } + + // Games + games := gamification.Group("/games") + { + games.POST("", r.gamificationHandler.CreateGame) + games.GET("", r.gamificationHandler.ListGames) + games.GET("/active", r.gamificationHandler.GetActiveGames) + games.GET("/:id", r.gamificationHandler.GetGame) + games.PUT("/:id", r.gamificationHandler.UpdateGame) + games.DELETE("/:id", r.gamificationHandler.DeleteGame) + } + + // Game Prizes + gamePrizes := gamification.Group("/game-prizes") + { + gamePrizes.POST("", r.gamificationHandler.CreateGamePrize) + gamePrizes.GET("", r.gamificationHandler.ListGamePrizes) + gamePrizes.GET("/:id", r.gamificationHandler.GetGamePrize) + gamePrizes.PUT("/:id", r.gamificationHandler.UpdateGamePrize) + gamePrizes.DELETE("/:id", r.gamificationHandler.DeleteGamePrize) + gamePrizes.GET("/game/:game_id", r.gamificationHandler.GetGamePrizesByGameID) + gamePrizes.GET("/game/:game_id/available", r.gamificationHandler.GetAvailablePrizes) + } + + //// Game Plays + //gamePlays := gamification.Group("/game-plays") + //{ + // gamePlays.POST("", r.gamificationHandler.CreateGamePlay) + // gamePlays.GET("", r.gamificationHandler.ListGamePlays) + // gamePlays.GET("/:id", r.gamificationHandler.GetGamePlay) + // gamePlays.POST("/play", r.gamificationHandler.PlayGame) + //} + + // Omset Tracker + //omsetTracker := gamification.Group("/omset-tracker") + //{ + // omsetTracker.POST("", r.gamificationHandler.CreateOmsetTracker) + // omsetTracker.GET("", r.gamificationHandler.ListOmsetTrackers) + // omsetTracker.GET("/:id", r.gamificationHandler.GetOmsetTracker) + // omsetTracker.PUT("/:id", r.gamificationHandler.UpdateOmsetTracker) + // omsetTracker.DELETE("/:id", r.gamificationHandler.DeleteOmsetTracker) + //} + } + outlets := protected.Group("/outlets") outlets.Use(r.authMiddleware.RequireAdminOrManager()) { diff --git a/internal/service/gamification_service.go b/internal/service/gamification_service.go new file mode 100644 index 0000000..6b5b22c --- /dev/null +++ b/internal/service/gamification_service.go @@ -0,0 +1,466 @@ +package service + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/transformer" + "context" + "time" + + "github.com/google/uuid" +) + +type GamificationService interface { + // Customer Points + CreateCustomerPoints(ctx context.Context, req *contract.CreateCustomerPointsRequest) (*contract.CustomerPointsResponse, error) + GetCustomerPoints(ctx context.Context, id uuid.UUID) (*contract.CustomerPointsResponse, error) + GetCustomerPointsByCustomerID(ctx context.Context, customerID uuid.UUID) (*contract.CustomerPointsResponse, error) + ListCustomerPoints(ctx context.Context, query *contract.ListCustomerPointsRequest) (*contract.PaginatedCustomerPointsResponse, error) + UpdateCustomerPoints(ctx context.Context, id uuid.UUID, req *contract.UpdateCustomerPointsRequest) (*contract.CustomerPointsResponse, error) + DeleteCustomerPoints(ctx context.Context, id uuid.UUID) error + AddCustomerPoints(ctx context.Context, customerID uuid.UUID, req *contract.AddCustomerPointsRequest) (*contract.CustomerPointsResponse, error) + DeductCustomerPoints(ctx context.Context, customerID uuid.UUID, req *contract.DeductCustomerPointsRequest) (*contract.CustomerPointsResponse, error) + + // Customer Tokens + CreateCustomerTokens(ctx context.Context, req *contract.CreateCustomerTokensRequest) (*contract.CustomerTokensResponse, error) + GetCustomerTokens(ctx context.Context, id uuid.UUID) (*contract.CustomerTokensResponse, error) + GetCustomerTokensByCustomerIDAndType(ctx context.Context, customerID uuid.UUID, tokenType string) (*contract.CustomerTokensResponse, error) + ListCustomerTokens(ctx context.Context, query *contract.ListCustomerTokensRequest) (*contract.PaginatedCustomerTokensResponse, error) + UpdateCustomerTokens(ctx context.Context, id uuid.UUID, req *contract.UpdateCustomerTokensRequest) (*contract.CustomerTokensResponse, error) + DeleteCustomerTokens(ctx context.Context, id uuid.UUID) error + AddCustomerTokens(ctx context.Context, customerID uuid.UUID, tokenType string, req *contract.AddCustomerTokensRequest) (*contract.CustomerTokensResponse, error) + DeductCustomerTokens(ctx context.Context, customerID uuid.UUID, tokenType string, req *contract.DeductCustomerTokensRequest) (*contract.CustomerTokensResponse, error) + + // Tiers + CreateTier(ctx context.Context, req *contract.CreateTierRequest) (*contract.TierResponse, error) + GetTier(ctx context.Context, id uuid.UUID) (*contract.TierResponse, error) + ListTiers(ctx context.Context, query *contract.ListTiersRequest) (*contract.PaginatedTiersResponse, error) + UpdateTier(ctx context.Context, id uuid.UUID, req *contract.UpdateTierRequest) (*contract.TierResponse, error) + DeleteTier(ctx context.Context, id uuid.UUID) error + GetTierByPoints(ctx context.Context, points int64) (*contract.TierResponse, error) + + // Games + CreateGame(ctx context.Context, req *contract.CreateGameRequest) (*contract.GameResponse, error) + GetGame(ctx context.Context, id uuid.UUID) (*contract.GameResponse, error) + ListGames(ctx context.Context, query *contract.ListGamesRequest) (*contract.PaginatedGamesResponse, error) + UpdateGame(ctx context.Context, id uuid.UUID, req *contract.UpdateGameRequest) (*contract.GameResponse, error) + DeleteGame(ctx context.Context, id uuid.UUID) error + GetActiveGames(ctx context.Context) ([]contract.GameResponse, error) + + // Game Prizes + CreateGamePrize(ctx context.Context, req *contract.CreateGamePrizeRequest) (*contract.GamePrizeResponse, error) + GetGamePrize(ctx context.Context, id uuid.UUID) (*contract.GamePrizeResponse, error) + GetGamePrizesByGameID(ctx context.Context, gameID uuid.UUID) ([]contract.GamePrizeResponse, error) + ListGamePrizes(ctx context.Context, query *contract.ListGamePrizesRequest) (*contract.PaginatedGamePrizesResponse, error) + UpdateGamePrize(ctx context.Context, id uuid.UUID, req *contract.UpdateGamePrizeRequest) (*contract.GamePrizeResponse, error) + DeleteGamePrize(ctx context.Context, id uuid.UUID) error + GetAvailablePrizes(ctx context.Context, gameID uuid.UUID) ([]contract.GamePrizeResponse, error) + + // Game Plays + CreateGamePlay(ctx context.Context, req *contract.CreateGamePlayRequest) (*contract.GamePlayResponse, error) + GetGamePlay(ctx context.Context, id uuid.UUID) (*contract.GamePlayResponse, error) + ListGamePlays(ctx context.Context, query *contract.ListGamePlaysRequest) (*contract.PaginatedGamePlaysResponse, error) + PlayGame(ctx context.Context, req *contract.PlayGameRequest) (*contract.PlayGameResponse, error) + + // Omset Tracker + CreateOmsetTracker(ctx context.Context, req *contract.CreateOmsetTrackerRequest) (*contract.OmsetTrackerResponse, error) + GetOmsetTracker(ctx context.Context, id uuid.UUID) (*contract.OmsetTrackerResponse, error) + ListOmsetTrackers(ctx context.Context, query *contract.ListOmsetTrackerRequest) (*contract.PaginatedOmsetTrackerResponse, error) + UpdateOmsetTracker(ctx context.Context, id uuid.UUID, req *contract.UpdateOmsetTrackerRequest) (*contract.OmsetTrackerResponse, error) + DeleteOmsetTracker(ctx context.Context, id uuid.UUID) error + AddOmset(ctx context.Context, periodType string, periodStart, periodEnd time.Time, amount int64, gameID *uuid.UUID) (*contract.OmsetTrackerResponse, error) +} + +type GamificationServiceImpl struct { + customerPointsProcessor *processor.CustomerPointsProcessor + customerTokensProcessor *processor.CustomerTokensProcessor + tierProcessor *processor.TierProcessor + gameProcessor *processor.GameProcessor + gamePrizeProcessor *processor.GamePrizeProcessor + gamePlayProcessor *processor.GamePlayProcessor + omsetTrackerProcessor *processor.OmsetTrackerProcessor +} + +func NewGamificationService( + customerPointsProcessor *processor.CustomerPointsProcessor, + customerTokensProcessor *processor.CustomerTokensProcessor, + tierProcessor *processor.TierProcessor, + gameProcessor *processor.GameProcessor, + gamePrizeProcessor *processor.GamePrizeProcessor, + gamePlayProcessor *processor.GamePlayProcessor, + omsetTrackerProcessor *processor.OmsetTrackerProcessor, +) *GamificationServiceImpl { + return &GamificationServiceImpl{ + customerPointsProcessor: customerPointsProcessor, + customerTokensProcessor: customerTokensProcessor, + tierProcessor: tierProcessor, + gameProcessor: gameProcessor, + gamePrizeProcessor: gamePrizeProcessor, + gamePlayProcessor: gamePlayProcessor, + omsetTrackerProcessor: omsetTrackerProcessor, + } +} + +// Customer Points Service Methods +func (s *GamificationServiceImpl) CreateCustomerPoints(ctx context.Context, req *contract.CreateCustomerPointsRequest) (*contract.CustomerPointsResponse, error) { + modelReq := transformer.CreateCustomerPointsRequestToModel(req) + response, err := s.customerPointsProcessor.CreateCustomerPoints(ctx, modelReq) + if err != nil { + return nil, err + } + return transformer.CustomerPointsModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) GetCustomerPoints(ctx context.Context, id uuid.UUID) (*contract.CustomerPointsResponse, error) { + response, err := s.customerPointsProcessor.GetCustomerPoints(ctx, id) + if err != nil { + return nil, err + } + return transformer.CustomerPointsModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) GetCustomerPointsByCustomerID(ctx context.Context, customerID uuid.UUID) (*contract.CustomerPointsResponse, error) { + response, err := s.customerPointsProcessor.GetCustomerPointsByCustomerID(ctx, customerID) + if err != nil { + return nil, err + } + return transformer.CustomerPointsModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) ListCustomerPoints(ctx context.Context, query *contract.ListCustomerPointsRequest) (*contract.PaginatedCustomerPointsResponse, error) { + modelQuery := transformer.ListCustomerPointsRequestToModel(query) + response, err := s.customerPointsProcessor.ListCustomerPoints(ctx, modelQuery) + if err != nil { + return nil, err + } + return transformer.PaginatedCustomerPointsResponseToContract(response), nil +} + +func (s *GamificationServiceImpl) UpdateCustomerPoints(ctx context.Context, id uuid.UUID, req *contract.UpdateCustomerPointsRequest) (*contract.CustomerPointsResponse, error) { + modelReq := transformer.UpdateCustomerPointsRequestToModel(req) + response, err := s.customerPointsProcessor.UpdateCustomerPoints(ctx, id, modelReq) + if err != nil { + return nil, err + } + return transformer.CustomerPointsModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) DeleteCustomerPoints(ctx context.Context, id uuid.UUID) error { + return s.customerPointsProcessor.DeleteCustomerPoints(ctx, id) +} + +func (s *GamificationServiceImpl) AddCustomerPoints(ctx context.Context, customerID uuid.UUID, req *contract.AddCustomerPointsRequest) (*contract.CustomerPointsResponse, error) { + response, err := s.customerPointsProcessor.AddPoints(ctx, customerID, req.Points) + if err != nil { + return nil, err + } + return transformer.CustomerPointsModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) DeductCustomerPoints(ctx context.Context, customerID uuid.UUID, req *contract.DeductCustomerPointsRequest) (*contract.CustomerPointsResponse, error) { + response, err := s.customerPointsProcessor.DeductPoints(ctx, customerID, req.Points) + if err != nil { + return nil, err + } + return transformer.CustomerPointsModelToResponse(response), nil +} + +// Customer Tokens Service Methods +func (s *GamificationServiceImpl) CreateCustomerTokens(ctx context.Context, req *contract.CreateCustomerTokensRequest) (*contract.CustomerTokensResponse, error) { + modelReq := transformer.CreateCustomerTokensRequestToModel(req) + response, err := s.customerTokensProcessor.CreateCustomerTokens(ctx, modelReq) + if err != nil { + return nil, err + } + return transformer.CustomerTokensModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) GetCustomerTokens(ctx context.Context, id uuid.UUID) (*contract.CustomerTokensResponse, error) { + response, err := s.customerTokensProcessor.GetCustomerTokens(ctx, id) + if err != nil { + return nil, err + } + return transformer.CustomerTokensModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) GetCustomerTokensByCustomerIDAndType(ctx context.Context, customerID uuid.UUID, tokenType string) (*contract.CustomerTokensResponse, error) { + response, err := s.customerTokensProcessor.GetCustomerTokensByCustomerIDAndType(ctx, customerID, tokenType) + if err != nil { + return nil, err + } + return transformer.CustomerTokensModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) ListCustomerTokens(ctx context.Context, query *contract.ListCustomerTokensRequest) (*contract.PaginatedCustomerTokensResponse, error) { + modelQuery := transformer.ListCustomerTokensRequestToModel(query) + response, err := s.customerTokensProcessor.ListCustomerTokens(ctx, modelQuery) + if err != nil { + return nil, err + } + return transformer.PaginatedCustomerTokensResponseToContract(response), nil +} + +func (s *GamificationServiceImpl) UpdateCustomerTokens(ctx context.Context, id uuid.UUID, req *contract.UpdateCustomerTokensRequest) (*contract.CustomerTokensResponse, error) { + modelReq := transformer.UpdateCustomerTokensRequestToModel(req) + response, err := s.customerTokensProcessor.UpdateCustomerTokens(ctx, id, modelReq) + if err != nil { + return nil, err + } + return transformer.CustomerTokensModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) DeleteCustomerTokens(ctx context.Context, id uuid.UUID) error { + return s.customerTokensProcessor.DeleteCustomerTokens(ctx, id) +} + +func (s *GamificationServiceImpl) AddCustomerTokens(ctx context.Context, customerID uuid.UUID, tokenType string, req *contract.AddCustomerTokensRequest) (*contract.CustomerTokensResponse, error) { + response, err := s.customerTokensProcessor.AddTokens(ctx, customerID, tokenType, req.Tokens) + if err != nil { + return nil, err + } + return transformer.CustomerTokensModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) DeductCustomerTokens(ctx context.Context, customerID uuid.UUID, tokenType string, req *contract.DeductCustomerTokensRequest) (*contract.CustomerTokensResponse, error) { + response, err := s.customerTokensProcessor.DeductTokens(ctx, customerID, tokenType, req.Tokens) + if err != nil { + return nil, err + } + return transformer.CustomerTokensModelToResponse(response), nil +} + +// Tier Service Methods +func (s *GamificationServiceImpl) CreateTier(ctx context.Context, req *contract.CreateTierRequest) (*contract.TierResponse, error) { + modelReq := transformer.CreateTierRequestToModel(req) + response, err := s.tierProcessor.CreateTier(ctx, modelReq) + if err != nil { + return nil, err + } + return transformer.TierModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) GetTier(ctx context.Context, id uuid.UUID) (*contract.TierResponse, error) { + response, err := s.tierProcessor.GetTier(ctx, id) + if err != nil { + return nil, err + } + return transformer.TierModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) ListTiers(ctx context.Context, query *contract.ListTiersRequest) (*contract.PaginatedTiersResponse, error) { + modelQuery := transformer.ListTiersRequestToModel(query) + response, err := s.tierProcessor.ListTiers(ctx, modelQuery) + if err != nil { + return nil, err + } + return transformer.PaginatedTiersResponseToContract(response), nil +} + +func (s *GamificationServiceImpl) UpdateTier(ctx context.Context, id uuid.UUID, req *contract.UpdateTierRequest) (*contract.TierResponse, error) { + modelReq := transformer.UpdateTierRequestToModel(req) + response, err := s.tierProcessor.UpdateTier(ctx, id, modelReq) + if err != nil { + return nil, err + } + return transformer.TierModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) DeleteTier(ctx context.Context, id uuid.UUID) error { + return s.tierProcessor.DeleteTier(ctx, id) +} + +func (s *GamificationServiceImpl) GetTierByPoints(ctx context.Context, points int64) (*contract.TierResponse, error) { + response, err := s.tierProcessor.GetTierByPoints(ctx, points) + if err != nil { + return nil, err + } + return transformer.TierModelToResponse(response), nil +} + +// Game Service Methods +func (s *GamificationServiceImpl) CreateGame(ctx context.Context, req *contract.CreateGameRequest) (*contract.GameResponse, error) { + modelReq := transformer.CreateGameRequestToModel(req) + response, err := s.gameProcessor.CreateGame(ctx, modelReq) + if err != nil { + return nil, err + } + return transformer.GameModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) GetGame(ctx context.Context, id uuid.UUID) (*contract.GameResponse, error) { + response, err := s.gameProcessor.GetGame(ctx, id) + if err != nil { + return nil, err + } + return transformer.GameModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) ListGames(ctx context.Context, query *contract.ListGamesRequest) (*contract.PaginatedGamesResponse, error) { + modelQuery := transformer.ListGamesRequestToModel(query) + response, err := s.gameProcessor.ListGames(ctx, modelQuery) + if err != nil { + return nil, err + } + return transformer.PaginatedGamesResponseToContract(response), nil +} + +func (s *GamificationServiceImpl) UpdateGame(ctx context.Context, id uuid.UUID, req *contract.UpdateGameRequest) (*contract.GameResponse, error) { + modelReq := transformer.UpdateGameRequestToModel(req) + response, err := s.gameProcessor.UpdateGame(ctx, id, modelReq) + if err != nil { + return nil, err + } + return transformer.GameModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) DeleteGame(ctx context.Context, id uuid.UUID) error { + return s.gameProcessor.DeleteGame(ctx, id) +} + +func (s *GamificationServiceImpl) GetActiveGames(ctx context.Context) ([]contract.GameResponse, error) { + response, err := s.gameProcessor.GetActiveGames(ctx) + if err != nil { + return nil, err + } + return transformer.GameModelsToResponses(response), nil +} + +// Game Prize Service Methods +func (s *GamificationServiceImpl) CreateGamePrize(ctx context.Context, req *contract.CreateGamePrizeRequest) (*contract.GamePrizeResponse, error) { + modelReq := transformer.CreateGamePrizeRequestToModel(req) + response, err := s.gamePrizeProcessor.CreateGamePrize(ctx, modelReq) + if err != nil { + return nil, err + } + return transformer.GamePrizeModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) GetGamePrize(ctx context.Context, id uuid.UUID) (*contract.GamePrizeResponse, error) { + response, err := s.gamePrizeProcessor.GetGamePrize(ctx, id) + if err != nil { + return nil, err + } + return transformer.GamePrizeModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) GetGamePrizesByGameID(ctx context.Context, gameID uuid.UUID) ([]contract.GamePrizeResponse, error) { + response, err := s.gamePrizeProcessor.GetGamePrizesByGameID(ctx, gameID) + if err != nil { + return nil, err + } + return transformer.GamePrizeModelsToResponses(response), nil +} + +func (s *GamificationServiceImpl) ListGamePrizes(ctx context.Context, query *contract.ListGamePrizesRequest) (*contract.PaginatedGamePrizesResponse, error) { + modelQuery := transformer.ListGamePrizesRequestToModel(query) + response, err := s.gamePrizeProcessor.ListGamePrizes(ctx, modelQuery) + if err != nil { + return nil, err + } + return transformer.PaginatedGamePrizesResponseToContract(response), nil +} + +func (s *GamificationServiceImpl) UpdateGamePrize(ctx context.Context, id uuid.UUID, req *contract.UpdateGamePrizeRequest) (*contract.GamePrizeResponse, error) { + modelReq := transformer.UpdateGamePrizeRequestToModel(req) + response, err := s.gamePrizeProcessor.UpdateGamePrize(ctx, id, modelReq) + if err != nil { + return nil, err + } + return transformer.GamePrizeModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) DeleteGamePrize(ctx context.Context, id uuid.UUID) error { + return s.gamePrizeProcessor.DeleteGamePrize(ctx, id) +} + +func (s *GamificationServiceImpl) GetAvailablePrizes(ctx context.Context, gameID uuid.UUID) ([]contract.GamePrizeResponse, error) { + response, err := s.gamePrizeProcessor.GetAvailablePrizes(ctx, gameID) + if err != nil { + return nil, err + } + return transformer.GamePrizeModelsToResponses(response), nil +} + +// Game Play Service Methods +func (s *GamificationServiceImpl) CreateGamePlay(ctx context.Context, req *contract.CreateGamePlayRequest) (*contract.GamePlayResponse, error) { + modelReq := transformer.CreateGamePlayRequestToModel(req) + response, err := s.gamePlayProcessor.CreateGamePlay(ctx, modelReq) + if err != nil { + return nil, err + } + return transformer.GamePlayModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) GetGamePlay(ctx context.Context, id uuid.UUID) (*contract.GamePlayResponse, error) { + response, err := s.gamePlayProcessor.GetGamePlay(ctx, id) + if err != nil { + return nil, err + } + return transformer.GamePlayModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) ListGamePlays(ctx context.Context, query *contract.ListGamePlaysRequest) (*contract.PaginatedGamePlaysResponse, error) { + modelQuery := transformer.ListGamePlaysRequestToModel(query) + response, err := s.gamePlayProcessor.ListGamePlays(ctx, modelQuery) + if err != nil { + return nil, err + } + return transformer.PaginatedGamePlaysResponseToContract(response), nil +} + +func (s *GamificationServiceImpl) PlayGame(ctx context.Context, req *contract.PlayGameRequest) (*contract.PlayGameResponse, error) { + modelReq := transformer.PlayGameRequestToModel(req) + response, err := s.gamePlayProcessor.PlayGame(ctx, modelReq) + if err != nil { + return nil, err + } + return transformer.PlayGameModelToResponse(response), nil +} + +// Omset Tracker Service Methods +func (s *GamificationServiceImpl) CreateOmsetTracker(ctx context.Context, req *contract.CreateOmsetTrackerRequest) (*contract.OmsetTrackerResponse, error) { + modelReq := transformer.CreateOmsetTrackerRequestToModel(req) + response, err := s.omsetTrackerProcessor.CreateOmsetTracker(ctx, modelReq) + if err != nil { + return nil, err + } + return transformer.OmsetTrackerModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) GetOmsetTracker(ctx context.Context, id uuid.UUID) (*contract.OmsetTrackerResponse, error) { + response, err := s.omsetTrackerProcessor.GetOmsetTracker(ctx, id) + if err != nil { + return nil, err + } + return transformer.OmsetTrackerModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) ListOmsetTrackers(ctx context.Context, query *contract.ListOmsetTrackerRequest) (*contract.PaginatedOmsetTrackerResponse, error) { + modelQuery := transformer.ListOmsetTrackerRequestToModel(query) + response, err := s.omsetTrackerProcessor.ListOmsetTrackers(ctx, modelQuery) + if err != nil { + return nil, err + } + return transformer.PaginatedOmsetTrackerResponseToContract(response), nil +} + +func (s *GamificationServiceImpl) UpdateOmsetTracker(ctx context.Context, id uuid.UUID, req *contract.UpdateOmsetTrackerRequest) (*contract.OmsetTrackerResponse, error) { + modelReq := transformer.UpdateOmsetTrackerRequestToModel(req) + response, err := s.omsetTrackerProcessor.UpdateOmsetTracker(ctx, id, modelReq) + if err != nil { + return nil, err + } + return transformer.OmsetTrackerModelToResponse(response), nil +} + +func (s *GamificationServiceImpl) DeleteOmsetTracker(ctx context.Context, id uuid.UUID) error { + return s.omsetTrackerProcessor.DeleteOmsetTracker(ctx, id) +} + +func (s *GamificationServiceImpl) AddOmset(ctx context.Context, periodType string, periodStart, periodEnd time.Time, amount int64, gameID *uuid.UUID) (*contract.OmsetTrackerResponse, error) { + response, err := s.omsetTrackerProcessor.AddOmset(ctx, periodType, periodStart, periodEnd, amount, gameID) + if err != nil { + return nil, err + } + return transformer.OmsetTrackerModelToResponse(response), nil +} diff --git a/internal/transformer/gamification_transformer.go b/internal/transformer/gamification_transformer.go new file mode 100644 index 0000000..a178466 --- /dev/null +++ b/internal/transformer/gamification_transformer.go @@ -0,0 +1,439 @@ +package transformer + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" +) + +// Customer Points Transformers +func CreateCustomerPointsRequestToModel(req *contract.CreateCustomerPointsRequest) *models.CreateCustomerPointsRequest { + return &models.CreateCustomerPointsRequest{ + CustomerID: req.CustomerID, + Balance: req.Balance, + } +} + +func UpdateCustomerPointsRequestToModel(req *contract.UpdateCustomerPointsRequest) *models.UpdateCustomerPointsRequest { + return &models.UpdateCustomerPointsRequest{ + Balance: req.Balance, + } +} + +func ListCustomerPointsRequestToModel(req *contract.ListCustomerPointsRequest) *models.ListCustomerPointsQuery { + return &models.ListCustomerPointsQuery{ + Page: req.Page, + Limit: req.Limit, + Search: req.Search, + SortBy: req.SortBy, + SortOrder: req.SortOrder, + } +} + +func CustomerPointsModelToResponse(model *models.CustomerPointsResponse) *contract.CustomerPointsResponse { + return &contract.CustomerPointsResponse{ + ID: model.ID, + CustomerID: model.CustomerID, + Balance: model.Balance, + Customer: CustomerModelToResponse(model.Customer), + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func PaginatedCustomerPointsResponseToContract(model *models.PaginatedResponse[models.CustomerPointsResponse]) *contract.PaginatedCustomerPointsResponse { + responses := make([]contract.CustomerPointsResponse, len(model.Data)) + for i, item := range model.Data { + responses[i] = *CustomerPointsModelToResponse(&item) + } + + return &contract.PaginatedCustomerPointsResponse{ + Data: responses, + TotalCount: int(model.Pagination.Total), + Page: model.Pagination.Page, + Limit: model.Pagination.Limit, + TotalPages: model.Pagination.TotalPages, + } +} + +// Customer Tokens Transformers +func CreateCustomerTokensRequestToModel(req *contract.CreateCustomerTokensRequest) *models.CreateCustomerTokensRequest { + return &models.CreateCustomerTokensRequest{ + CustomerID: req.CustomerID, + TokenType: req.TokenType, + Balance: req.Balance, + } +} + +func UpdateCustomerTokensRequestToModel(req *contract.UpdateCustomerTokensRequest) *models.UpdateCustomerTokensRequest { + return &models.UpdateCustomerTokensRequest{ + Balance: req.Balance, + } +} + +func ListCustomerTokensRequestToModel(req *contract.ListCustomerTokensRequest) *models.ListCustomerTokensQuery { + return &models.ListCustomerTokensQuery{ + Page: req.Page, + Limit: req.Limit, + Search: req.Search, + TokenType: req.TokenType, + SortBy: req.SortBy, + SortOrder: req.SortOrder, + } +} + +func CustomerTokensModelToResponse(model *models.CustomerTokensResponse) *contract.CustomerTokensResponse { + return &contract.CustomerTokensResponse{ + ID: model.ID, + CustomerID: model.CustomerID, + TokenType: model.TokenType, + Balance: model.Balance, + Customer: CustomerModelToResponse(model.Customer), + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func PaginatedCustomerTokensResponseToContract(model *models.PaginatedResponse[models.CustomerTokensResponse]) *contract.PaginatedCustomerTokensResponse { + responses := make([]contract.CustomerTokensResponse, len(model.Data)) + for i, item := range model.Data { + responses[i] = *CustomerTokensModelToResponse(&item) + } + + return &contract.PaginatedCustomerTokensResponse{ + Data: responses, + TotalCount: int(model.Pagination.Total), + Page: model.Pagination.Page, + Limit: model.Pagination.Limit, + TotalPages: model.Pagination.TotalPages, + } +} + +// Tier Transformers +func CreateTierRequestToModel(req *contract.CreateTierRequest) *models.CreateTierRequest { + return &models.CreateTierRequest{ + Name: req.Name, + MinPoints: req.MinPoints, + Benefits: req.Benefits, + } +} + +func UpdateTierRequestToModel(req *contract.UpdateTierRequest) *models.UpdateTierRequest { + return &models.UpdateTierRequest{ + Name: req.Name, + MinPoints: req.MinPoints, + Benefits: req.Benefits, + } +} + +func ListTiersRequestToModel(req *contract.ListTiersRequest) *models.ListTiersQuery { + return &models.ListTiersQuery{ + Page: req.Page, + Limit: req.Limit, + Search: req.Search, + SortBy: req.SortBy, + SortOrder: req.SortOrder, + } +} + +func TierModelToResponse(model *models.TierResponse) *contract.TierResponse { + return &contract.TierResponse{ + ID: model.ID, + Name: model.Name, + MinPoints: model.MinPoints, + Benefits: model.Benefits, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func PaginatedTiersResponseToContract(model *models.PaginatedResponse[models.TierResponse]) *contract.PaginatedTiersResponse { + responses := make([]contract.TierResponse, len(model.Data)) + for i, item := range model.Data { + responses[i] = *TierModelToResponse(&item) + } + + return &contract.PaginatedTiersResponse{ + Data: responses, + TotalCount: int(model.Pagination.Total), + Page: model.Pagination.Page, + Limit: model.Pagination.Limit, + TotalPages: model.Pagination.TotalPages, + } +} + +// Game Transformers +func CreateGameRequestToModel(req *contract.CreateGameRequest) *models.CreateGameRequest { + return &models.CreateGameRequest{ + Name: req.Name, + Type: req.Type, + IsActive: req.IsActive, + Metadata: req.Metadata, + } +} + +func UpdateGameRequestToModel(req *contract.UpdateGameRequest) *models.UpdateGameRequest { + return &models.UpdateGameRequest{ + Name: req.Name, + Type: req.Type, + IsActive: req.IsActive, + Metadata: req.Metadata, + } +} + +func ListGamesRequestToModel(req *contract.ListGamesRequest) *models.ListGamesQuery { + return &models.ListGamesQuery{ + Page: req.Page, + Limit: req.Limit, + Search: req.Search, + Type: req.Type, + IsActive: req.IsActive, + SortBy: req.SortBy, + SortOrder: req.SortOrder, + } +} + +func GameModelToResponse(model *models.GameResponse) *contract.GameResponse { + return &contract.GameResponse{ + ID: model.ID, + Name: model.Name, + Type: model.Type, + IsActive: model.IsActive, + Metadata: model.Metadata, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func GameModelsToResponses(models []models.GameResponse) []contract.GameResponse { + responses := make([]contract.GameResponse, len(models)) + for i, model := range models { + responses[i] = *GameModelToResponse(&model) + } + return responses +} + +func PaginatedGamesResponseToContract(model *models.PaginatedResponse[models.GameResponse]) *contract.PaginatedGamesResponse { + responses := make([]contract.GameResponse, len(model.Data)) + for i, item := range model.Data { + responses[i] = *GameModelToResponse(&item) + } + + return &contract.PaginatedGamesResponse{ + Data: responses, + TotalCount: int(model.Pagination.Total), + Page: model.Pagination.Page, + Limit: model.Pagination.Limit, + TotalPages: model.Pagination.TotalPages, + } +} + +// Game Prize Transformers +func CreateGamePrizeRequestToModel(req *contract.CreateGamePrizeRequest) *models.CreateGamePrizeRequest { + return &models.CreateGamePrizeRequest{ + GameID: req.GameID, + Name: req.Name, + Weight: req.Weight, + Stock: req.Stock, + MaxStock: req.MaxStock, + Threshold: req.Threshold, + FallbackPrizeID: req.FallbackPrizeID, + Metadata: req.Metadata, + } +} + +func UpdateGamePrizeRequestToModel(req *contract.UpdateGamePrizeRequest) *models.UpdateGamePrizeRequest { + return &models.UpdateGamePrizeRequest{ + Name: req.Name, + Weight: req.Weight, + Stock: req.Stock, + MaxStock: req.MaxStock, + Threshold: req.Threshold, + FallbackPrizeID: req.FallbackPrizeID, + Metadata: req.Metadata, + } +} + +func ListGamePrizesRequestToModel(req *contract.ListGamePrizesRequest) *models.ListGamePrizesQuery { + return &models.ListGamePrizesQuery{ + Page: req.Page, + Limit: req.Limit, + Search: req.Search, + GameID: req.GameID, + SortBy: req.SortBy, + SortOrder: req.SortOrder, + } +} + +func GamePrizeModelToResponse(model *models.GamePrizeResponse) *contract.GamePrizeResponse { + return &contract.GamePrizeResponse{ + ID: model.ID, + GameID: model.GameID, + Name: model.Name, + Weight: model.Weight, + Stock: model.Stock, + MaxStock: model.MaxStock, + Threshold: model.Threshold, + FallbackPrizeID: model.FallbackPrizeID, + Metadata: model.Metadata, + Game: GameModelToResponse(model.Game), + FallbackPrize: GamePrizeModelToResponse(model.FallbackPrize), + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func GamePrizeModelsToResponses(models []models.GamePrizeResponse) []contract.GamePrizeResponse { + responses := make([]contract.GamePrizeResponse, len(models)) + for i, model := range models { + responses[i] = *GamePrizeModelToResponse(&model) + } + return responses +} + +func PaginatedGamePrizesResponseToContract(model *models.PaginatedResponse[models.GamePrizeResponse]) *contract.PaginatedGamePrizesResponse { + responses := make([]contract.GamePrizeResponse, len(model.Data)) + for i, item := range model.Data { + responses[i] = *GamePrizeModelToResponse(&item) + } + + return &contract.PaginatedGamePrizesResponse{ + Data: responses, + TotalCount: int(model.Pagination.Total), + Page: model.Pagination.Page, + Limit: model.Pagination.Limit, + TotalPages: model.Pagination.TotalPages, + } +} + +// Game Play Transformers +func CreateGamePlayRequestToModel(req *contract.CreateGamePlayRequest) *models.CreateGamePlayRequest { + return &models.CreateGamePlayRequest{ + GameID: req.GameID, + CustomerID: req.CustomerID, + TokenUsed: req.TokenUsed, + RandomSeed: req.RandomSeed, + } +} + +func PlayGameRequestToModel(req *contract.PlayGameRequest) *models.PlayGameRequest { + return &models.PlayGameRequest{ + GameID: req.GameID, + CustomerID: req.CustomerID, + TokenUsed: req.TokenUsed, + } +} + +func ListGamePlaysRequestToModel(req *contract.ListGamePlaysRequest) *models.ListGamePlaysQuery { + return &models.ListGamePlaysQuery{ + Page: req.Page, + Limit: req.Limit, + Search: req.Search, + GameID: req.GameID, + CustomerID: req.CustomerID, + PrizeID: req.PrizeID, + SortBy: req.SortBy, + SortOrder: req.SortOrder, + } +} + +func GamePlayModelToResponse(model *models.GamePlayResponse) *contract.GamePlayResponse { + return &contract.GamePlayResponse{ + ID: model.ID, + GameID: model.GameID, + CustomerID: model.CustomerID, + PrizeID: model.PrizeID, + TokenUsed: model.TokenUsed, + RandomSeed: model.RandomSeed, + CreatedAt: model.CreatedAt, + Game: GameModelToResponse(model.Game), + Customer: CustomerModelToResponse(model.Customer), + Prize: GamePrizeModelToResponse(model.Prize), + } +} + +func PlayGameModelToResponse(model *models.PlayGameResponse) *contract.PlayGameResponse { + return &contract.PlayGameResponse{ + GamePlay: *GamePlayModelToResponse(&model.GamePlay), + PrizeWon: GamePrizeModelToResponse(model.PrizeWon), + TokensRemaining: model.TokensRemaining, + } +} + +func PaginatedGamePlaysResponseToContract(model *models.PaginatedResponse[models.GamePlayResponse]) *contract.PaginatedGamePlaysResponse { + responses := make([]contract.GamePlayResponse, len(model.Data)) + for i, item := range model.Data { + responses[i] = *GamePlayModelToResponse(&item) + } + + return &contract.PaginatedGamePlaysResponse{ + Data: responses, + TotalCount: int(model.Pagination.Total), + Page: model.Pagination.Page, + Limit: model.Pagination.Limit, + TotalPages: model.Pagination.TotalPages, + } +} + +// Omset Tracker Transformers +func CreateOmsetTrackerRequestToModel(req *contract.CreateOmsetTrackerRequest) *models.CreateOmsetTrackerRequest { + return &models.CreateOmsetTrackerRequest{ + PeriodType: req.PeriodType, + PeriodStart: req.PeriodStart, + PeriodEnd: req.PeriodEnd, + Total: req.Total, + GameID: req.GameID, + } +} + +func UpdateOmsetTrackerRequestToModel(req *contract.UpdateOmsetTrackerRequest) *models.UpdateOmsetTrackerRequest { + return &models.UpdateOmsetTrackerRequest{ + PeriodType: req.PeriodType, + PeriodStart: req.PeriodStart, + PeriodEnd: req.PeriodEnd, + Total: req.Total, + GameID: req.GameID, + } +} + +func ListOmsetTrackerRequestToModel(req *contract.ListOmsetTrackerRequest) *models.ListOmsetTrackerQuery { + return &models.ListOmsetTrackerQuery{ + Page: req.Page, + Limit: req.Limit, + Search: req.Search, + PeriodType: req.PeriodType, + GameID: req.GameID, + From: req.From, + To: req.To, + SortBy: req.SortBy, + SortOrder: req.SortOrder, + } +} + +func OmsetTrackerModelToResponse(model *models.OmsetTrackerResponse) *contract.OmsetTrackerResponse { + return &contract.OmsetTrackerResponse{ + ID: model.ID, + PeriodType: model.PeriodType, + PeriodStart: model.PeriodStart, + PeriodEnd: model.PeriodEnd, + Total: model.Total, + GameID: model.GameID, + Game: GameModelToResponse(model.Game), + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func PaginatedOmsetTrackerResponseToContract(model *models.PaginatedResponse[models.OmsetTrackerResponse]) *contract.PaginatedOmsetTrackerResponse { + responses := make([]contract.OmsetTrackerResponse, len(model.Data)) + for i, item := range model.Data { + responses[i] = *OmsetTrackerModelToResponse(&item) + } + + return &contract.PaginatedOmsetTrackerResponse{ + Data: responses, + TotalCount: int(model.Pagination.Total), + Page: model.Pagination.Page, + Limit: model.Pagination.Limit, + TotalPages: model.Pagination.TotalPages, + } +} diff --git a/internal/validator/gamification_validator.go b/internal/validator/gamification_validator.go new file mode 100644 index 0000000..c65a001 --- /dev/null +++ b/internal/validator/gamification_validator.go @@ -0,0 +1,556 @@ +package validator + +import ( + "apskel-pos-be/internal/contract" + "errors" + "strings" + + "github.com/go-playground/validator/v10" +) + +type GamificationValidator interface { + // Customer Points + ValidateCreateCustomerPointsRequest(req *contract.CreateCustomerPointsRequest) (error, string) + ValidateUpdateCustomerPointsRequest(req *contract.UpdateCustomerPointsRequest) (error, string) + ValidateListCustomerPointsRequest(req *contract.ListCustomerPointsRequest) (error, string) + ValidateAddCustomerPointsRequest(req *contract.AddCustomerPointsRequest) (error, string) + ValidateDeductCustomerPointsRequest(req *contract.DeductCustomerPointsRequest) (error, string) + + // Customer Tokens + ValidateCreateCustomerTokensRequest(req *contract.CreateCustomerTokensRequest) (error, string) + ValidateUpdateCustomerTokensRequest(req *contract.UpdateCustomerTokensRequest) (error, string) + ValidateListCustomerTokensRequest(req *contract.ListCustomerTokensRequest) (error, string) + ValidateAddCustomerTokensRequest(req *contract.AddCustomerTokensRequest) (error, string) + ValidateDeductCustomerTokensRequest(req *contract.DeductCustomerTokensRequest) (error, string) + + // Tiers + ValidateCreateTierRequest(req *contract.CreateTierRequest) (error, string) + ValidateUpdateTierRequest(req *contract.UpdateTierRequest) (error, string) + ValidateListTiersRequest(req *contract.ListTiersRequest) (error, string) + + // Games + ValidateCreateGameRequest(req *contract.CreateGameRequest) (error, string) + ValidateUpdateGameRequest(req *contract.UpdateGameRequest) (error, string) + ValidateListGamesRequest(req *contract.ListGamesRequest) (error, string) + + // Game Prizes + ValidateCreateGamePrizeRequest(req *contract.CreateGamePrizeRequest) (error, string) + ValidateUpdateGamePrizeRequest(req *contract.UpdateGamePrizeRequest) (error, string) + ValidateListGamePrizesRequest(req *contract.ListGamePrizesRequest) (error, string) + + // Game Plays + ValidateCreateGamePlayRequest(req *contract.CreateGamePlayRequest) (error, string) + ValidateListGamePlaysRequest(req *contract.ListGamePlaysRequest) (error, string) + ValidatePlayGameRequest(req *contract.PlayGameRequest) (error, string) + + // Omset Tracker + ValidateCreateOmsetTrackerRequest(req *contract.CreateOmsetTrackerRequest) (error, string) + ValidateUpdateOmsetTrackerRequest(req *contract.UpdateOmsetTrackerRequest) (error, string) + ValidateListOmsetTrackerRequest(req *contract.ListOmsetTrackerRequest) (error, string) + ValidateAddOmsetRequest(req *contract.AddOmsetRequest) (error, string) +} + +type GamificationValidatorImpl struct { + validate *validator.Validate +} + +func NewGamificationValidator() *GamificationValidatorImpl { + return &GamificationValidatorImpl{ + validate: validator.New(), + } +} + +// Customer Points Validators +func (v *GamificationValidatorImpl) ValidateCreateCustomerPointsRequest(req *contract.CreateCustomerPointsRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Balance < 0 { + return errors.New("balance cannot be negative"), "INVALID_BALANCE" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateUpdateCustomerPointsRequest(req *contract.UpdateCustomerPointsRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Balance < 0 { + return errors.New("balance cannot be negative"), "INVALID_BALANCE" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateListCustomerPointsRequest(req *contract.ListCustomerPointsRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Page <= 0 { + req.Page = 1 + } + if req.Limit <= 0 { + req.Limit = 10 + } + if req.Limit > 100 { + req.Limit = 100 + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateAddCustomerPointsRequest(req *contract.AddCustomerPointsRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Points <= 0 { + return errors.New("points must be greater than 0"), "INVALID_POINTS" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateDeductCustomerPointsRequest(req *contract.DeductCustomerPointsRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Points <= 0 { + return errors.New("points must be greater than 0"), "INVALID_POINTS" + } + + return nil, "" +} + +// Customer Tokens Validators +func (v *GamificationValidatorImpl) ValidateCreateCustomerTokensRequest(req *contract.CreateCustomerTokensRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Balance < 0 { + return errors.New("balance cannot be negative"), "INVALID_BALANCE" + } + + validTokenTypes := []string{"SPIN", "RAFFLE", "MINIGAME"} + if !contains(validTokenTypes, req.TokenType) { + return errors.New("invalid token type"), "INVALID_TOKEN_TYPE" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateUpdateCustomerTokensRequest(req *contract.UpdateCustomerTokensRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Balance < 0 { + return errors.New("balance cannot be negative"), "INVALID_BALANCE" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateListCustomerTokensRequest(req *contract.ListCustomerTokensRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Page <= 0 { + req.Page = 1 + } + if req.Limit <= 0 { + req.Limit = 10 + } + if req.Limit > 100 { + req.Limit = 100 + } + + if req.TokenType != "" { + validTokenTypes := []string{"SPIN", "RAFFLE", "MINIGAME"} + if !contains(validTokenTypes, req.TokenType) { + return errors.New("invalid token type"), "INVALID_TOKEN_TYPE" + } + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateAddCustomerTokensRequest(req *contract.AddCustomerTokensRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Tokens <= 0 { + return errors.New("tokens must be greater than 0"), "INVALID_TOKENS" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateDeductCustomerTokensRequest(req *contract.DeductCustomerTokensRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Tokens <= 0 { + return errors.New("tokens must be greater than 0"), "INVALID_TOKENS" + } + + return nil, "" +} + +// Tier Validators +func (v *GamificationValidatorImpl) ValidateCreateTierRequest(req *contract.CreateTierRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Name != "" { + req.Name = strings.TrimSpace(req.Name) + if req.Name == "" { + return errors.New("name cannot be empty or whitespace only"), "INVALID_NAME" + } + if len(req.Name) > 100 { + return errors.New("name cannot exceed 100 characters"), "INVALID_NAME" + } + } + + if req.MinPoints < 0 { + return errors.New("min points cannot be negative"), "INVALID_MIN_POINTS" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateUpdateTierRequest(req *contract.UpdateTierRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Name != nil && *req.Name != "" { + *req.Name = strings.TrimSpace(*req.Name) + if *req.Name == "" { + return errors.New("name cannot be empty or whitespace only"), "INVALID_NAME" + } + if len(*req.Name) > 100 { + return errors.New("name cannot exceed 100 characters"), "INVALID_NAME" + } + } + + if req.MinPoints != nil && *req.MinPoints < 0 { + return errors.New("min points cannot be negative"), "INVALID_MIN_POINTS" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateListTiersRequest(req *contract.ListTiersRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Page <= 0 { + req.Page = 1 + } + if req.Limit <= 0 { + req.Limit = 10 + } + if req.Limit > 100 { + req.Limit = 100 + } + + return nil, "" +} + +// Game Validators +func (v *GamificationValidatorImpl) ValidateCreateGameRequest(req *contract.CreateGameRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Name != "" { + req.Name = strings.TrimSpace(req.Name) + if req.Name == "" { + return errors.New("name cannot be empty or whitespace only"), "INVALID_NAME" + } + if len(req.Name) > 255 { + return errors.New("name cannot exceed 255 characters"), "INVALID_NAME" + } + } + + validGameTypes := []string{"SPIN", "RAFFLE", "MINIGAME"} + if !contains(validGameTypes, req.Type) { + return errors.New("invalid game type"), "INVALID_GAME_TYPE" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateUpdateGameRequest(req *contract.UpdateGameRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Name != nil && *req.Name != "" { + *req.Name = strings.TrimSpace(*req.Name) + if *req.Name == "" { + return errors.New("name cannot be empty or whitespace only"), "INVALID_NAME" + } + if len(*req.Name) > 255 { + return errors.New("name cannot exceed 255 characters"), "INVALID_NAME" + } + } + + if req.Type != nil { + validGameTypes := []string{"SPIN", "RAFFLE", "MINIGAME"} + if !contains(validGameTypes, *req.Type) { + return errors.New("invalid game type"), "INVALID_GAME_TYPE" + } + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateListGamesRequest(req *contract.ListGamesRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Page <= 0 { + req.Page = 1 + } + if req.Limit <= 0 { + req.Limit = 10 + } + if req.Limit > 100 { + req.Limit = 100 + } + + if req.Type != "" { + validGameTypes := []string{"SPIN", "RAFFLE", "MINIGAME"} + if !contains(validGameTypes, req.Type) { + return errors.New("invalid game type"), "INVALID_GAME_TYPE" + } + } + + return nil, "" +} + +// Game Prize Validators +func (v *GamificationValidatorImpl) ValidateCreateGamePrizeRequest(req *contract.CreateGamePrizeRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Name != "" { + req.Name = strings.TrimSpace(req.Name) + if req.Name == "" { + return errors.New("name cannot be empty or whitespace only"), "INVALID_NAME" + } + if len(req.Name) > 255 { + return errors.New("name cannot exceed 255 characters"), "INVALID_NAME" + } + } + + if req.Weight <= 0 { + return errors.New("weight must be greater than 0"), "INVALID_WEIGHT" + } + + if req.Stock < 0 { + return errors.New("stock cannot be negative"), "INVALID_STOCK" + } + + if req.MaxStock != nil && *req.MaxStock <= 0 { + return errors.New("max stock must be greater than 0"), "INVALID_MAX_STOCK" + } + + if req.Threshold != nil && *req.Threshold < 0 { + return errors.New("threshold cannot be negative"), "INVALID_THRESHOLD" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateUpdateGamePrizeRequest(req *contract.UpdateGamePrizeRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Name != nil && *req.Name != "" { + *req.Name = strings.TrimSpace(*req.Name) + if *req.Name == "" { + return errors.New("name cannot be empty or whitespace only"), "INVALID_NAME" + } + if len(*req.Name) > 255 { + return errors.New("name cannot exceed 255 characters"), "INVALID_NAME" + } + } + + if req.Weight != nil && *req.Weight <= 0 { + return errors.New("weight must be greater than 0"), "INVALID_WEIGHT" + } + + if req.Stock != nil && *req.Stock < 0 { + return errors.New("stock cannot be negative"), "INVALID_STOCK" + } + + if req.MaxStock != nil && *req.MaxStock <= 0 { + return errors.New("max stock must be greater than 0"), "INVALID_MAX_STOCK" + } + + if req.Threshold != nil && *req.Threshold < 0 { + return errors.New("threshold cannot be negative"), "INVALID_THRESHOLD" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateListGamePrizesRequest(req *contract.ListGamePrizesRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Page <= 0 { + req.Page = 1 + } + if req.Limit <= 0 { + req.Limit = 10 + } + if req.Limit > 100 { + req.Limit = 100 + } + + return nil, "" +} + +// Game Play Validators +func (v *GamificationValidatorImpl) ValidateCreateGamePlayRequest(req *contract.CreateGamePlayRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.TokenUsed < 0 { + return errors.New("token used cannot be negative"), "INVALID_TOKEN_USED" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateListGamePlaysRequest(req *contract.ListGamePlaysRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Page <= 0 { + req.Page = 1 + } + if req.Limit <= 0 { + req.Limit = 10 + } + if req.Limit > 100 { + req.Limit = 100 + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidatePlayGameRequest(req *contract.PlayGameRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.TokenUsed < 0 { + return errors.New("token used cannot be negative"), "INVALID_TOKEN_USED" + } + + return nil, "" +} + +// Omset Tracker Validators +func (v *GamificationValidatorImpl) ValidateCreateOmsetTrackerRequest(req *contract.CreateOmsetTrackerRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + validPeriodTypes := []string{"DAILY", "WEEKLY", "MONTHLY", "TOTAL"} + if !contains(validPeriodTypes, req.PeriodType) { + return errors.New("invalid period type"), "INVALID_PERIOD_TYPE" + } + + if req.Total < 0 { + return errors.New("total cannot be negative"), "INVALID_TOTAL" + } + + if req.PeriodEnd.Before(req.PeriodStart) { + return errors.New("period end must be after period start"), "INVALID_PERIOD" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateUpdateOmsetTrackerRequest(req *contract.UpdateOmsetTrackerRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.PeriodType != nil { + validPeriodTypes := []string{"DAILY", "WEEKLY", "MONTHLY", "TOTAL"} + if !contains(validPeriodTypes, *req.PeriodType) { + return errors.New("invalid period type"), "INVALID_PERIOD_TYPE" + } + } + + if req.Total != nil && *req.Total < 0 { + return errors.New("total cannot be negative"), "INVALID_TOTAL" + } + + if req.PeriodStart != nil && req.PeriodEnd != nil && req.PeriodEnd.Before(*req.PeriodStart) { + return errors.New("period end must be after period start"), "INVALID_PERIOD" + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateListOmsetTrackerRequest(req *contract.ListOmsetTrackerRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Page <= 0 { + req.Page = 1 + } + if req.Limit <= 0 { + req.Limit = 10 + } + if req.Limit > 100 { + req.Limit = 100 + } + + if req.PeriodType != "" { + validPeriodTypes := []string{"DAILY", "WEEKLY", "MONTHLY", "TOTAL"} + if !contains(validPeriodTypes, req.PeriodType) { + return errors.New("invalid period type"), "INVALID_PERIOD_TYPE" + } + } + + return nil, "" +} + +func (v *GamificationValidatorImpl) ValidateAddOmsetRequest(req *contract.AddOmsetRequest) (error, string) { + if err := v.validate.Struct(req); err != nil { + return err, "VALIDATION_ERROR" + } + + if req.Amount <= 0 { + return errors.New("amount must be greater than 0"), "INVALID_AMOUNT" + } + + return nil, "" +} diff --git a/migrations/000048_create_customer_points_table.down.sql b/migrations/000048_create_customer_points_table.down.sql new file mode 100644 index 0000000..f26eeb8 --- /dev/null +++ b/migrations/000048_create_customer_points_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS customer_points; diff --git a/migrations/000048_create_customer_points_table.up.sql b/migrations/000048_create_customer_points_table.up.sql new file mode 100644 index 0000000..fb0f721 --- /dev/null +++ b/migrations/000048_create_customer_points_table.up.sql @@ -0,0 +1,22 @@ +CREATE TABLE customer_points ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + customer_id UUID NOT NULL, + balance BIGINT DEFAULT 0 NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + CONSTRAINT fk_customer_points_customer + FOREIGN KEY (customer_id) + REFERENCES customers(id) + ON DELETE CASCADE, + + CONSTRAINT chk_customer_points_balance_non_negative + CHECK (balance >= 0) +); + +-- Create indexes +CREATE INDEX idx_customer_points_customer_id ON customer_points(customer_id); +CREATE INDEX idx_customer_points_updated_at ON customer_points(updated_at); + +-- Create unique constraint to ensure one point record per customer +CREATE UNIQUE INDEX idx_customer_points_unique_customer ON customer_points(customer_id); diff --git a/migrations/000049_create_customer_tokens_table.down.sql b/migrations/000049_create_customer_tokens_table.down.sql new file mode 100644 index 0000000..44a1ca0 --- /dev/null +++ b/migrations/000049_create_customer_tokens_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS customer_tokens; diff --git a/migrations/000049_create_customer_tokens_table.up.sql b/migrations/000049_create_customer_tokens_table.up.sql new file mode 100644 index 0000000..77df7b5 --- /dev/null +++ b/migrations/000049_create_customer_tokens_table.up.sql @@ -0,0 +1,25 @@ +CREATE TABLE customer_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + customer_id UUID NOT NULL, + token_type VARCHAR(50) NOT NULL, + balance BIGINT DEFAULT 0 NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + CONSTRAINT fk_customer_tokens_customer + FOREIGN KEY (customer_id) + REFERENCES customers(id) + ON DELETE CASCADE, + + CONSTRAINT chk_customer_tokens_balance_non_negative + CHECK (balance >= 0), + + CONSTRAINT chk_customer_tokens_type_valid + CHECK (token_type IN ('SPIN', 'RAFFLE', 'MINIGAME')) +); + +CREATE INDEX idx_customer_tokens_customer_id ON customer_tokens(customer_id); +CREATE INDEX idx_customer_tokens_token_type ON customer_tokens(token_type); +CREATE INDEX idx_customer_tokens_updated_at ON customer_tokens(updated_at); + +CREATE UNIQUE INDEX idx_customer_tokens_unique_customer_type ON customer_tokens(customer_id, token_type); diff --git a/migrations/000050_create_tiers_table.down.sql b/migrations/000050_create_tiers_table.down.sql new file mode 100644 index 0000000..04d842c --- /dev/null +++ b/migrations/000050_create_tiers_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS tiers; diff --git a/migrations/000050_create_tiers_table.up.sql b/migrations/000050_create_tiers_table.up.sql new file mode 100644 index 0000000..11220aa --- /dev/null +++ b/migrations/000050_create_tiers_table.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE tiers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) NOT NULL UNIQUE, + min_points BIGINT NOT NULL, + benefits JSONB DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + CONSTRAINT chk_tiers_min_points_non_negative + CHECK (min_points >= 0) +); + +CREATE INDEX idx_tiers_min_points ON tiers(min_points); +CREATE INDEX idx_tiers_created_at ON tiers(created_at); diff --git a/migrations/000051_create_games_table.down.sql b/migrations/000051_create_games_table.down.sql new file mode 100644 index 0000000..df130f5 --- /dev/null +++ b/migrations/000051_create_games_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS games; diff --git a/migrations/000051_create_games_table.up.sql b/migrations/000051_create_games_table.up.sql new file mode 100644 index 0000000..8a3cbfd --- /dev/null +++ b/migrations/000051_create_games_table.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE games ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + metadata JSONB DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + CONSTRAINT chk_games_type_valid + CHECK (type IN ('SPIN', 'RAFFLE', 'MINIGAME')) +); + +-- Create indexes +CREATE INDEX idx_games_type ON games(type); +CREATE INDEX idx_games_is_active ON games(is_active); +CREATE INDEX idx_games_created_at ON games(created_at); diff --git a/migrations/000052_create_game_prizes_table.down.sql b/migrations/000052_create_game_prizes_table.down.sql new file mode 100644 index 0000000..7c0f3cd --- /dev/null +++ b/migrations/000052_create_game_prizes_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS game_prizes; diff --git a/migrations/000052_create_game_prizes_table.up.sql b/migrations/000052_create_game_prizes_table.up.sql new file mode 100644 index 0000000..9420a99 --- /dev/null +++ b/migrations/000052_create_game_prizes_table.up.sql @@ -0,0 +1,45 @@ +CREATE TABLE game_prizes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + game_id UUID NOT NULL, + name VARCHAR(255) NOT NULL, + weight INTEGER NOT NULL, + stock INTEGER DEFAULT 0, + max_stock INTEGER, + threshold BIGINT, + fallback_prize_id UUID, + metadata JSONB DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + CONSTRAINT fk_game_prizes_game + FOREIGN KEY (game_id) + REFERENCES games(id) + ON DELETE CASCADE, + + CONSTRAINT fk_game_prizes_fallback + FOREIGN KEY (fallback_prize_id) + REFERENCES game_prizes(id) + ON DELETE SET NULL, + + CONSTRAINT chk_game_prizes_weight_positive + CHECK (weight > 0), + + CONSTRAINT chk_game_prizes_stock_non_negative + CHECK (stock >= 0), + + CONSTRAINT chk_game_prizes_max_stock_positive + CHECK (max_stock IS NULL OR max_stock > 0), + + CONSTRAINT chk_game_prizes_threshold_non_negative + CHECK (threshold IS NULL OR threshold >= 0), + + CONSTRAINT chk_game_prizes_stock_not_exceed_max + CHECK (max_stock IS NULL OR stock <= max_stock) +); + +-- Create indexes +CREATE INDEX idx_game_prizes_game_id ON game_prizes(game_id); +CREATE INDEX idx_game_prizes_weight ON game_prizes(weight); +CREATE INDEX idx_game_prizes_stock ON game_prizes(stock); +CREATE INDEX idx_game_prizes_fallback_prize_id ON game_prizes(fallback_prize_id); +CREATE INDEX idx_game_prizes_created_at ON game_prizes(created_at); diff --git a/migrations/000053_create_game_plays_table.down.sql b/migrations/000053_create_game_plays_table.down.sql new file mode 100644 index 0000000..3db4662 --- /dev/null +++ b/migrations/000053_create_game_plays_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS game_plays; diff --git a/migrations/000053_create_game_plays_table.up.sql b/migrations/000053_create_game_plays_table.up.sql new file mode 100644 index 0000000..ff38fcf --- /dev/null +++ b/migrations/000053_create_game_plays_table.up.sql @@ -0,0 +1,34 @@ +CREATE TABLE game_plays ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + game_id UUID NOT NULL, + customer_id UUID NOT NULL, + prize_id UUID, + token_used INTEGER DEFAULT 0, + random_seed VARCHAR(255), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + CONSTRAINT fk_game_plays_game + FOREIGN KEY (game_id) + REFERENCES games(id) + ON DELETE CASCADE, + + CONSTRAINT fk_game_plays_customer + FOREIGN KEY (customer_id) + REFERENCES customers(id) + ON DELETE CASCADE, + + CONSTRAINT fk_game_plays_prize + FOREIGN KEY (prize_id) + REFERENCES game_prizes(id) + ON DELETE SET NULL, + + CONSTRAINT chk_game_plays_token_used_non_negative + CHECK (token_used >= 0) +); + +-- Create indexes +CREATE INDEX idx_game_plays_game_id ON game_plays(game_id); +CREATE INDEX idx_game_plays_customer_id ON game_plays(customer_id); +CREATE INDEX idx_game_plays_prize_id ON game_plays(prize_id); +CREATE INDEX idx_game_plays_created_at ON game_plays(created_at); +CREATE INDEX idx_game_plays_game_customer ON game_plays(game_id, customer_id); diff --git a/migrations/000054_create_omset_tracker_table.down.sql b/migrations/000054_create_omset_tracker_table.down.sql new file mode 100644 index 0000000..c21815e --- /dev/null +++ b/migrations/000054_create_omset_tracker_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS omset_tracker; diff --git a/migrations/000054_create_omset_tracker_table.up.sql b/migrations/000054_create_omset_tracker_table.up.sql new file mode 100644 index 0000000..9b09b22 --- /dev/null +++ b/migrations/000054_create_omset_tracker_table.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE omset_tracker ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + period_type VARCHAR(20) NOT NULL, + period_start DATE NOT NULL, + period_end DATE NOT NULL, + total BIGINT DEFAULT 0 NOT NULL, + game_id UUID, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + CONSTRAINT fk_omset_tracker_game + FOREIGN KEY (game_id) + REFERENCES games(id) + ON DELETE SET NULL, + + CONSTRAINT chk_omset_tracker_period_type_valid + CHECK (period_type IN ('DAILY', 'WEEKLY', 'MONTHLY', 'TOTAL')), + + CONSTRAINT chk_omset_tracker_total_non_negative + CHECK (total >= 0), + + CONSTRAINT chk_omset_tracker_period_valid + CHECK (period_end >= period_start) +); + +-- Create indexes +CREATE INDEX idx_omset_tracker_period_type ON omset_tracker(period_type); +CREATE INDEX idx_omset_tracker_period_start ON omset_tracker(period_start); +CREATE INDEX idx_omset_tracker_game_id ON omset_tracker(game_id); +CREATE INDEX idx_omset_tracker_created_at ON omset_tracker(created_at); +CREATE INDEX idx_omset_tracker_period_type_start ON omset_tracker(period_type, period_start); +CREATE INDEX idx_omset_tracker_game_period ON omset_tracker(game_id, period_type, period_start); diff --git a/server b/server deleted file mode 100755 index 3ed1e65..0000000 Binary files a/server and /dev/null differ