From a520d0ed119354960e5343ac20eb8197534b6a99 Mon Sep 17 00:00:00 2001 From: Aditya Siregar Date: Thu, 18 Sep 2025 13:39:37 +0700 Subject: [PATCH] fix campaign rules --- internal/app/app.go | 12 +- internal/constants/error.go | 23 +-- internal/contract/campaign_contract.go | 2 - internal/contract/spin_game_contract.go | 20 +++ internal/handler/campaign_handler.go | 128 ++++++++++++++++ internal/handler/spin_game_handler.go | 62 ++++++++ internal/mappers/campaign_mapper.go | 23 +-- internal/mappers/game_play_mapper.go | 9 ++ internal/mappers/spin_game_mapper.go | 80 ++++++++++ internal/models/spin_game.go | 37 +++++ internal/processor/game_play_processor.go | 6 +- internal/repository/campaign_repository.go | 43 +----- internal/repository/game_play_repository.go | 161 +++++++++++++------- internal/router/router.go | 15 ++ internal/service/campaign_service.go | 156 +++++++++++++++++-- internal/service/spin_game_service.go | 98 ++++++++++++ internal/validator/campaign_validator.go | 22 --- 17 files changed, 733 insertions(+), 164 deletions(-) create mode 100644 internal/contract/spin_game_contract.go create mode 100644 internal/handler/spin_game_handler.go create mode 100644 internal/mappers/spin_game_mapper.go create mode 100644 internal/models/spin_game.go create mode 100644 internal/service/spin_game_service.go diff --git a/internal/app/app.go b/internal/app/app.go index a9eb6ab..5f4084e 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -103,6 +103,7 @@ func (a *App) Initialize(cfg *config.Config) error { services.customerAuthService, validators.customerAuthValidator, services.customerPointsService, + services.spinGameService, middleware.customerAuthMiddleware, ) @@ -181,10 +182,11 @@ type repositories struct { tierRepo *repository.TierRepository gameRepo *repository.GameRepository gamePrizeRepo *repository.GamePrizeRepository - gamePlayRepo *repository.GamePlayRepository + gamePlayRepo repository.GamePlayRepository omsetTrackerRepo *repository.OmsetTrackerRepository rewardRepo repository.RewardRepository campaignRepo repository.CampaignRepository + campaignRuleRepo repository.CampaignRuleRepository customerAuthRepo repository.CustomerAuthRepository customerPointsRepo repository.CustomerPointsRepository otpRepo repository.OtpRepository @@ -229,6 +231,7 @@ func (a *App) initRepositories() *repositories { omsetTrackerRepo: repository.NewOmsetTrackerRepository(a.db), rewardRepo: repository.NewRewardRepository(a.db), campaignRepo: repository.NewCampaignRepository(a.db), + campaignRuleRepo: repository.NewCampaignRuleRepository(a.db), customerAuthRepo: repository.NewCustomerAuthRepository(a.db), customerPointsRepo: repository.NewCustomerPointsRepository(a.db), otpRepo: repository.NewOtpRepository(a.db), @@ -269,6 +272,7 @@ type processors struct { omsetTrackerProcessor *processor.OmsetTrackerProcessor rewardProcessor processor.RewardProcessor campaignProcessor processor.CampaignProcessor + campaignRuleProcessor processor.CampaignRuleProcessor customerAuthProcessor processor.CustomerAuthProcessor customerPointsProcessor *processor.CustomerPointsProcessor otpProcessor processor.OtpProcessor @@ -315,6 +319,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor omsetTrackerProcessor: processor.NewOmsetTrackerProcessor(repos.omsetTrackerRepo), rewardProcessor: processor.NewRewardProcessor(repos.rewardRepo), campaignProcessor: processor.NewCampaignProcessor(repos.campaignRepo), + campaignRuleProcessor: processor.NewCampaignRuleProcessor(repos.campaignRuleRepo), customerAuthProcessor: processor.NewCustomerAuthProcessor(repos.customerAuthRepo, otpProcessor, repos.otpRepo, cfg.GetCustomerJWTSecret(), cfg.GetCustomerJWTExpiresTTL()), customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo, repos.gameRepo), otpProcessor: otpProcessor, @@ -355,6 +360,7 @@ type services struct { campaignService service.CampaignService customerAuthService service.CustomerAuthService customerPointsService service.CustomerPointsService + spinGameService service.SpinGameService } func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services { @@ -387,9 +393,10 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con orderIngredientTransactionService := service.NewOrderIngredientTransactionService(processors.orderIngredientTransactionProcessor, repos.txManager) gamificationService := service.NewGamificationService(processors.customerPointsProcessor, processors.customerTokensProcessor, processors.tierProcessor, processors.gameProcessor, processors.gamePrizeProcessor, processors.gamePlayProcessor, processors.omsetTrackerProcessor) rewardService := service.NewRewardService(processors.rewardProcessor) - campaignService := service.NewCampaignService(processors.campaignProcessor) + campaignService := service.NewCampaignService(processors.campaignProcessor, processors.campaignRuleProcessor) customerAuthService := service.NewCustomerAuthService(processors.customerAuthProcessor) customerPointsService := service.NewCustomerPointsService(processors.customerPointsProcessor) + spinGameService := service.NewSpinGameService(processors.gamePlayProcessor, repos.txManager) // Update order service with order ingredient transaction service orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager) @@ -426,6 +433,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con campaignService: campaignService, customerAuthService: customerAuthService, customerPointsService: customerPointsService, + spinGameService: spinGameService, } } diff --git a/internal/constants/error.go b/internal/constants/error.go index 5209cef..cc8d499 100644 --- a/internal/constants/error.go +++ b/internal/constants/error.go @@ -43,17 +43,18 @@ const ( 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" - RewardEntity = "reward" - CampaignEntity = "campaign" - CampaignRuleEntity = "campaign_rule" - CustomerEntity = "customer" + CustomerPointsEntity = "customer_points" + CustomerTokensEntity = "customer_tokens" + TierEntity = "tier" + GameEntity = "game" + GamePrizeEntity = "game_prize" + GamePlayEntity = "game_play" + OmsetTrackerEntity = "omset_tracker" + RewardEntity = "reward" + CampaignEntity = "campaign" + CampaignRuleEntity = "campaign_rule" + CustomerEntity = "customer" + SpinGameHandlerEntity = "spin_game_handler" ) var HttpErrorMap = map[string]int{ diff --git a/internal/contract/campaign_contract.go b/internal/contract/campaign_contract.go index 28b8ddd..d64bbb5 100644 --- a/internal/contract/campaign_contract.go +++ b/internal/contract/campaign_contract.go @@ -17,7 +17,6 @@ type CreateCampaignRequest struct { ShowOnApp bool `json:"show_on_app"` Position int `json:"position" binding:"min=0"` Metadata *map[string]interface{} `json:"metadata,omitempty"` - Rules []CampaignRuleStruct `json:"rules" binding:"required,min=1"` } type UpdateCampaignRequest struct { @@ -31,7 +30,6 @@ type UpdateCampaignRequest struct { ShowOnApp bool `json:"show_on_app"` Position int `json:"position" binding:"min=0"` Metadata *map[string]interface{} `json:"metadata,omitempty"` - Rules []CampaignRuleStruct `json:"rules" binding:"required,min=1"` } type ListCampaignsRequest struct { diff --git a/internal/contract/spin_game_contract.go b/internal/contract/spin_game_contract.go new file mode 100644 index 0000000..e41dc41 --- /dev/null +++ b/internal/contract/spin_game_contract.go @@ -0,0 +1,20 @@ +package contract + +// SpinGameRequest represents the request to play a spin game +type SpinGameRequest struct { + SpinID string `json:"spin_id" validate:"required,uuid"` +} + +// SpinGameResponse represents the response from playing a spin game +type SpinGameResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Data *SpinGameResponseData `json:"data,omitempty"` +} + +// SpinGameResponseData contains the game play result +type SpinGameResponseData struct { + GamePlay GamePlayResponse `json:"game_play"` + PrizeWon *CustomerGamePrizeResponse `json:"prize_won,omitempty"` + TokensRemaining int64 `json:"tokens_remaining"` +} diff --git a/internal/handler/campaign_handler.go b/internal/handler/campaign_handler.go index 4546e99..3603726 100644 --- a/internal/handler/campaign_handler.go +++ b/internal/handler/campaign_handler.go @@ -177,3 +177,131 @@ func (h *CampaignHandler) GetCampaignsForApp(c *gin.Context) { util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CampaignHandler::GetCampaignsForApp") } + +// Campaign Rules Handlers + +func (h *CampaignHandler) CreateCampaignRule(c *gin.Context) { + ctx := c.Request.Context() + + var req contract.CreateCampaignRuleRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::CreateCampaignRule -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::CreateCampaignRule") + return + } + + response, err := h.campaignService.CreateCampaignRule(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::CreateCampaignRule -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CampaignRuleEntity, err.Error())}), "CampaignHandler::CreateCampaignRule") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CampaignHandler::CreateCampaignRule") +} + +func (h *CampaignHandler) GetCampaignRule(c *gin.Context) { + ctx := c.Request.Context() + + idStr := c.Param("id") + if idStr == "" { + logger.FromContext(c.Request.Context()).Error("CampaignHandler::GetCampaignRule -> missing ID parameter") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.CampaignRuleEntity, "ID parameter is required") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::GetCampaignRule") + return + } + + response, err := h.campaignService.GetCampaignRule(ctx, idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::GetCampaignRule -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CampaignRuleEntity, err.Error())}), "CampaignHandler::GetCampaignRule") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CampaignHandler::GetCampaignRule") +} + +func (h *CampaignHandler) ListCampaignRules(c *gin.Context) { + ctx := c.Request.Context() + + var req contract.ListCampaignRulesRequest + if err := c.ShouldBindQuery(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::ListCampaignRules -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::ListCampaignRules") + return + } + + response, err := h.campaignService.ListCampaignRules(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::ListCampaignRules -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CampaignRuleEntity, err.Error())}), "CampaignHandler::ListCampaignRules") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CampaignHandler::ListCampaignRules") +} + +func (h *CampaignHandler) UpdateCampaignRule(c *gin.Context) { + ctx := c.Request.Context() + + var req contract.UpdateCampaignRuleRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::UpdateCampaignRule -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::UpdateCampaignRule") + return + } + + response, err := h.campaignService.UpdateCampaignRule(ctx, &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::UpdateCampaignRule -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CampaignRuleEntity, err.Error())}), "CampaignHandler::UpdateCampaignRule") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CampaignHandler::UpdateCampaignRule") +} + +func (h *CampaignHandler) DeleteCampaignRule(c *gin.Context) { + ctx := c.Request.Context() + + idStr := c.Param("id") + if idStr == "" { + logger.FromContext(c.Request.Context()).Error("CampaignHandler::DeleteCampaignRule -> missing ID parameter") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.CampaignRuleEntity, "ID parameter is required") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::DeleteCampaignRule") + return + } + + err := h.campaignService.DeleteCampaignRule(ctx, idStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::DeleteCampaignRule -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CampaignRuleEntity, err.Error())}), "CampaignHandler::DeleteCampaignRule") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse("Campaign rule deleted successfully"), "CampaignHandler::DeleteCampaignRule") +} + +func (h *CampaignHandler) GetCampaignRulesByCampaignID(c *gin.Context) { + ctx := c.Request.Context() + + campaignIDStr := c.Param("campaign_id") + if campaignIDStr == "" { + logger.FromContext(c.Request.Context()).Error("CampaignHandler::GetCampaignRulesByCampaignID -> missing campaign_id parameter") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.CampaignRuleEntity, "campaign_id parameter is required") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CampaignHandler::GetCampaignRulesByCampaignID") + return + } + + response, err := h.campaignService.GetCampaignRulesByCampaignID(ctx, campaignIDStr) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CampaignHandler::GetCampaignRulesByCampaignID -> service call failed") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CampaignRuleEntity, err.Error())}), "CampaignHandler::GetCampaignRulesByCampaignID") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CampaignHandler::GetCampaignRulesByCampaignID") +} diff --git a/internal/handler/spin_game_handler.go b/internal/handler/spin_game_handler.go new file mode 100644 index 0000000..3b218cb --- /dev/null +++ b/internal/handler/spin_game_handler.go @@ -0,0 +1,62 @@ +package handler + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/util" + + "github.com/gin-gonic/gin" +) + +type SpinGameHandler struct { + spinGameService service.SpinGameService +} + +func NewSpinGameHandler(spinGameService service.SpinGameService) *SpinGameHandler { + return &SpinGameHandler{ + spinGameService: spinGameService, + } +} + +// PlaySpinGame handles the spin game play request +func (h *SpinGameHandler) PlaySpinGame(c *gin.Context) { + // Get customer ID from JWT token (set by middleware) + customerID, exists := c.Get("customer_id") + if !exists { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.ValidationErrorCode, constants.SpinGameHandlerEntity, "Customer ID not found in context"), + }), "SpinGameHandler::PlaySpinGame") + return + } + + // Parse spin ID from request body + var req contract.SpinGameRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.ValidationErrorCode, constants.SpinGameHandlerEntity, err.Error()), + }), "SpinGameHandler::PlaySpinGame") + return + } + + // Convert to model + modelReq := &models.SpinGameRequest{ + SpinID: req.SpinID, + } + + // Play the spin game + response, err := h.spinGameService.PlaySpinGame(c.Request.Context(), customerID.(string), modelReq) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError(constants.InternalServerErrorCode, constants.SpinGameHandlerEntity, err.Error()), + }), "SpinGameHandler::PlaySpinGame") + return + } + + // Convert model response to contract response + contractResponse := mappers.SpinGameResponseModelToContract(response) + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResponse.Data), "SpinGameHandler::PlaySpinGame") +} diff --git a/internal/mappers/campaign_mapper.go b/internal/mappers/campaign_mapper.go index 7978464..f662108 100644 --- a/internal/mappers/campaign_mapper.go +++ b/internal/mappers/campaign_mapper.go @@ -68,14 +68,6 @@ func ToCampaignEntity(request *contract.CreateCampaignRequest) *entities.Campaig return nil } - var rules []entities.CampaignRule - if request.Rules != nil { - for _, rule := range request.Rules { - // For create request, we'll use a temporary UUID that will be set by the repository - rules = append(rules, *ToCampaignRuleEntity(&rule, uuid.Nil)) - } - } - return &entities.Campaign{ Name: request.Name, Description: request.Description, @@ -86,7 +78,7 @@ func ToCampaignEntity(request *contract.CreateCampaignRequest) *entities.Campaig ShowOnApp: request.ShowOnApp, Position: request.Position, Metadata: request.Metadata, - Rules: rules, + Rules: nil, // Rules will be managed separately } } @@ -95,13 +87,6 @@ func ToCampaignEntityFromUpdate(request *contract.UpdateCampaignRequest) *entiti return nil } - var rules []entities.CampaignRule - if request.Rules != nil { - for _, rule := range request.Rules { - rules = append(rules, *ToCampaignRuleEntityFromUpdate(&rule, request.ID)) - } - } - return &entities.Campaign{ ID: request.ID, Name: request.Name, @@ -113,7 +98,7 @@ func ToCampaignEntityFromUpdate(request *contract.UpdateCampaignRequest) *entiti ShowOnApp: request.ShowOnApp, Position: request.Position, Metadata: request.Metadata, - Rules: rules, + Rules: nil, // Rules will be managed separately } } @@ -129,7 +114,7 @@ func ToCampaignRuleEntity(request *contract.CampaignRuleStruct, campaignID uuid. } return &entities.CampaignRule{ - ID: uuid.New(), // Generate unique UUID for each campaign rule + ID: uuid.Nil, // Let GORM's BeforeCreate hook generate the UUID CampaignID: campaignID, RuleType: entities.RuleType(request.RuleType), ConditionValue: request.ConditionValue, @@ -153,7 +138,7 @@ func ToCampaignRuleEntityFromUpdate(request *contract.CampaignRuleStruct, campai } return &entities.CampaignRule{ - ID: uuid.New(), // Generate new UUID for each campaign rule + ID: uuid.Nil, // Let GORM's BeforeCreate hook generate the UUID CampaignID: campaignID, RuleType: entities.RuleType(request.RuleType), ConditionValue: request.ConditionValue, diff --git a/internal/mappers/game_play_mapper.go b/internal/mappers/game_play_mapper.go index b64c532..cf88268 100644 --- a/internal/mappers/game_play_mapper.go +++ b/internal/mappers/game_play_mapper.go @@ -34,6 +34,15 @@ func ToGamePlayResponses(gamePlays []entities.GamePlay) []models.GamePlayRespons return responses } +// ToGamePlayResponsesFromPointers converts a slice of game play entity pointers to game play responses +func ToGamePlayResponsesFromPointers(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{ diff --git a/internal/mappers/spin_game_mapper.go b/internal/mappers/spin_game_mapper.go new file mode 100644 index 0000000..be5b882 --- /dev/null +++ b/internal/mappers/spin_game_mapper.go @@ -0,0 +1,80 @@ +package mappers + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" +) + +// SpinGameRequestContractToModel converts spin game request from contract to model +func SpinGameRequestContractToModel(req *contract.SpinGameRequest) *models.SpinGameRequest { + if req == nil { + return nil + } + + return &models.SpinGameRequest{ + SpinID: req.SpinID, + } +} + +// SpinGameResponseModelToContract converts spin game response from model to contract +func SpinGameResponseModelToContract(resp *models.SpinGameResponse) *contract.SpinGameResponse { + if resp == nil { + return nil + } + + return &contract.SpinGameResponse{ + Status: resp.Status, + Message: resp.Message, + Data: SpinGameResponseDataModelToContract(resp.Data), + } +} + +// SpinGameResponseDataModelToContract converts spin game response data from model to contract +func SpinGameResponseDataModelToContract(data *models.SpinGameResponseData) *contract.SpinGameResponseData { + if data == nil { + return nil + } + + return &contract.SpinGameResponseData{ + GamePlay: GamePlayResponseModelToContract(&data.GamePlay), + PrizeWon: CustomerGamePrizeResponseModelToContract(data.PrizeWon), + TokensRemaining: data.TokensRemaining, + } +} + +// GamePlayResponseModelToContract converts game play response from model to contract +func GamePlayResponseModelToContract(resp *models.GamePlayResponse) contract.GamePlayResponse { + if resp == nil { + return contract.GamePlayResponse{} + } + + return contract.GamePlayResponse{ + ID: resp.ID, + GameID: resp.GameID, + CustomerID: resp.CustomerID, + PrizeID: resp.PrizeID, + TokenUsed: resp.TokenUsed, + RandomSeed: resp.RandomSeed, + CreatedAt: resp.CreatedAt, + Game: nil, // Optional field - can be populated separately if needed + Customer: nil, // Optional field - can be populated separately if needed + Prize: nil, // Optional field - can be populated separately if needed + } +} + +// CustomerGamePrizeResponseModelToContract converts customer game prize response from model to contract +func CustomerGamePrizeResponseModelToContract(resp *models.CustomerGamePrizeResponse) *contract.CustomerGamePrizeResponse { + if resp == nil { + return nil + } + + return &contract.CustomerGamePrizeResponse{ + ID: resp.ID, + GameID: resp.GameID, + Name: resp.Name, + Image: resp.Image, + Metadata: resp.Metadata, + CreatedAt: resp.CreatedAt, + UpdatedAt: resp.UpdatedAt, + } +} diff --git a/internal/models/spin_game.go b/internal/models/spin_game.go new file mode 100644 index 0000000..ae66946 --- /dev/null +++ b/internal/models/spin_game.go @@ -0,0 +1,37 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +// SpinGameRequest represents the request to play a spin game +type SpinGameRequest struct { + SpinID string `json:"spin_id" validate:"required,uuid"` +} + +// SpinGameResponse represents the response from playing a spin game +type SpinGameResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Data *SpinGameResponseData `json:"data,omitempty"` +} + +// SpinGameResponseData contains the game play result +type SpinGameResponseData struct { + GamePlay GamePlayResponse `json:"game_play"` + PrizeWon *CustomerGamePrizeResponse `json:"prize_won,omitempty"` + TokensRemaining int64 `json:"tokens_remaining"` +} + +// SpinGamePrizeResponse represents a prize won in the spin game +type SpinGamePrizeResponse struct { + ID uuid.UUID `json:"id"` + GameID uuid.UUID `json:"game_id"` + Name string `json:"name"` + Image *string `json:"image,omitempty"` + Metadata *map[string]interface{} `json:"metadata,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/internal/processor/game_play_processor.go b/internal/processor/game_play_processor.go index 25a4cd0..4a77282 100644 --- a/internal/processor/game_play_processor.go +++ b/internal/processor/game_play_processor.go @@ -15,7 +15,7 @@ import ( ) type GamePlayProcessor struct { - gamePlayRepo *repository.GamePlayRepository + gamePlayRepo repository.GamePlayRepository gameRepo *repository.GameRepository gamePrizeRepo *repository.GamePrizeRepository customerTokensRepo *repository.CustomerTokensRepository @@ -23,7 +23,7 @@ type GamePlayProcessor struct { } func NewGamePlayProcessor( - gamePlayRepo *repository.GamePlayRepository, + gamePlayRepo repository.GamePlayRepository, gameRepo *repository.GameRepository, gamePrizeRepo *repository.GamePrizeRepository, customerTokensRepo *repository.CustomerTokensRepository, @@ -94,7 +94,7 @@ func (p *GamePlayProcessor) ListGamePlays(ctx context.Context, query *models.Lis } // Convert to responses - responses := mappers.ToGamePlayResponses(gamePlays) + responses := mappers.ToGamePlayResponsesFromPointers(gamePlays) // Calculate pagination info totalPages := int((total + int64(query.Limit) - 1) / int64(query.Limit)) diff --git a/internal/repository/campaign_repository.go b/internal/repository/campaign_repository.go index 71a8b1f..ebfb19d 100644 --- a/internal/repository/campaign_repository.go +++ b/internal/repository/campaign_repository.go @@ -30,24 +30,7 @@ func NewCampaignRepository(db *gorm.DB) CampaignRepository { } func (r *campaignRepository) Create(ctx context.Context, campaign *entities.Campaign) error { - return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - // Create campaign first - if err := tx.Create(campaign).Error; err != nil { - return fmt.Errorf("failed to create campaign: %w", err) - } - - // Create campaign rules - if len(campaign.Rules) > 0 { - for i := range campaign.Rules { - campaign.Rules[i].CampaignID = campaign.ID - } - if err := tx.Create(&campaign.Rules).Error; err != nil { - return fmt.Errorf("failed to create campaign rules: %w", err) - } - } - - return nil - }) + return r.db.WithContext(ctx).Create(campaign).Error } func (r *campaignRepository) GetByID(ctx context.Context, id string) (*entities.Campaign, error) { @@ -114,29 +97,7 @@ func (r *campaignRepository) List(ctx context.Context, req *entities.ListCampaig } func (r *campaignRepository) Update(ctx context.Context, campaign *entities.Campaign) error { - return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - // Update campaign - if err := tx.Save(campaign).Error; err != nil { - return fmt.Errorf("failed to update campaign: %w", err) - } - - // Delete existing rules - if err := tx.Where("campaign_id = ?", campaign.ID).Delete(&entities.CampaignRule{}).Error; err != nil { - return fmt.Errorf("failed to delete existing campaign rules: %w", err) - } - - // Create new rules - if len(campaign.Rules) > 0 { - for i := range campaign.Rules { - campaign.Rules[i].CampaignID = campaign.ID - } - if err := tx.Create(&campaign.Rules).Error; err != nil { - return fmt.Errorf("failed to create campaign rules: %w", err) - } - } - - return nil - }) + return r.db.WithContext(ctx).Save(campaign).Error } func (r *campaignRepository) Delete(ctx context.Context, id string) error { diff --git a/internal/repository/game_play_repository.go b/internal/repository/game_play_repository.go index a35c25d..b3ffb2c 100644 --- a/internal/repository/game_play_repository.go +++ b/internal/repository/game_play_repository.go @@ -1,111 +1,170 @@ package repository import ( - "apskel-pos-be/internal/entities" "context" "fmt" + "apskel-pos-be/internal/entities" + "github.com/google/uuid" "gorm.io/gorm" ) -type GamePlayRepository struct { +type GamePlayRepository interface { + Create(ctx context.Context, gamePlay *entities.GamePlay) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.GamePlay, error) + List(ctx context.Context, offset, limit int, search string, gameID, customerID, prizeID *uuid.UUID, sortBy, sortOrder string) ([]*entities.GamePlay, int64, error) + GetByCustomerID(ctx context.Context, customerID uuid.UUID, limit int) ([]*entities.GamePlay, error) + GetByGameID(ctx context.Context, gameID uuid.UUID, limit int) ([]*entities.GamePlay, error) + CountByCustomerID(ctx context.Context, customerID uuid.UUID) (int64, error) + CountByGameID(ctx context.Context, gameID uuid.UUID) (int64, error) +} + +type gamePlayRepository struct { db *gorm.DB } -func NewGamePlayRepository(db *gorm.DB) *GamePlayRepository { - return &GamePlayRepository{db: 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) Create(ctx context.Context, gamePlay *entities.GamePlay) error { + if err := r.db.WithContext(ctx).Create(gamePlay).Error; err != nil { + return fmt.Errorf("failed to create game play: %w", err) + } + return nil } -func (r *GamePlayRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.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 + err := r.db.WithContext(ctx). + Preload("Game"). + Preload("Customer"). + Preload("Prize"). + First(&gamePlay, "id = ?", id).Error if err != nil { - return nil, err + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("game play not found") + } + return nil, fmt.Errorf("failed to get game play: %w", 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 +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") + query := r.db.WithContext(ctx).Model(&entities.GamePlay{}) + // Apply filters 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) + query = query.Where("random_seed ILIKE ?", "%"+search+"%") } - 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 + // Count total records + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count game plays: %w", err) } + // Apply sorting if sortBy != "" { - if sortOrder == "" { - sortOrder = "asc" + if sortOrder == "desc" { + query = query.Order(fmt.Sprintf("%s DESC", sortBy)) + } else { + query = query.Order(fmt.Sprintf("%s ASC", sortBy)) } - query = query.Order(fmt.Sprintf("game_plays.%s %s", sortBy, sortOrder)) } else { - query = query.Order("game_plays.created_at DESC") + query = query.Order("created_at DESC") } - err := query.Offset(offset).Limit(limit).Find(&gamePlays).Error + // Apply pagination and preload relations + err := query. + Preload("Game"). + Preload("Customer"). + Preload("Prize"). + Offset(offset). + Limit(limit). + Find(&gamePlays).Error + if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("failed to list game plays: %w", 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"). +func (r *gamePlayRepository) GetByCustomerID(ctx context.Context, customerID uuid.UUID, limit int) ([]*entities.GamePlay, error) { + var gamePlays []*entities.GamePlay + query := r.db.WithContext(ctx). Where("customer_id = ?", customerID). - Order("created_at DESC"). - Limit(limit). - Find(&gamePlays).Error - if err != nil { - return nil, err + Preload("Game"). + Preload("Prize"). + Order("created_at DESC") + + if limit > 0 { + query = query.Limit(limit) } + + err := query.Find(&gamePlays).Error + if err != nil { + return nil, fmt.Errorf("failed to get game plays by customer ID: %w", 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"). +func (r *gamePlayRepository) GetByGameID(ctx context.Context, gameID uuid.UUID, limit int) ([]*entities.GamePlay, error) { + var gamePlays []*entities.GamePlay + query := r.db.WithContext(ctx). Where("game_id = ?", gameID). - Scan(&stats).Error - if err != nil { - return nil, err + Preload("Customer"). + Preload("Prize"). + Order("created_at DESC") + + if limit > 0 { + query = query.Limit(limit) } - return stats, nil + err := query.Find(&gamePlays).Error + if err != nil { + return nil, fmt.Errorf("failed to get game plays by game ID: %w", err) + } + + return gamePlays, nil +} + +func (r *gamePlayRepository) CountByCustomerID(ctx context.Context, customerID uuid.UUID) (int64, error) { + var count int64 + err := r.db.WithContext(ctx). + Model(&entities.GamePlay{}). + Where("customer_id = ?", customerID). + Count(&count).Error + if err != nil { + return 0, fmt.Errorf("failed to count game plays by customer ID: %w", err) + } + return count, nil +} + +func (r *gamePlayRepository) CountByGameID(ctx context.Context, gameID uuid.UUID) (int64, error) { + var count int64 + err := r.db.WithContext(ctx). + Model(&entities.GamePlay{}). + Where("game_id = ?", gameID). + Count(&count).Error + if err != nil { + return 0, fmt.Errorf("failed to count game plays by game ID: %w", err) + } + return count, nil } diff --git a/internal/router/router.go b/internal/router/router.go index b548ff1..c19adc0 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -45,6 +45,7 @@ type Router struct { campaignHandler *handler.CampaignHandler customerAuthHandler *handler.CustomerAuthHandler customerPointsHandler *handler.CustomerPointsHandler + spinGameHandler *handler.SpinGameHandler authMiddleware *middleware.AuthMiddleware customerAuthMiddleware *middleware.CustomerAuthMiddleware } @@ -106,6 +107,7 @@ func NewRouter(cfg *config.Config, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, + spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware) *Router { return &Router{ @@ -141,6 +143,7 @@ func NewRouter(cfg *config.Config, campaignHandler: handler.NewCampaignHandler(campaignService, campaignValidator), customerAuthHandler: handler.NewCustomerAuthHandler(customerAuthService, customerAuthValidator), customerPointsHandler: handler.NewCustomerPointsHandler(customerPointsService), + spinGameHandler: handler.NewSpinGameHandler(spinGameService), authMiddleware: authMiddleware, customerAuthMiddleware: customerAuthMiddleware, } @@ -196,6 +199,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { customer.GET("/wallet", r.customerPointsHandler.GetCustomerWallet) customer.GET("/games", r.customerPointsHandler.GetCustomerGames) customer.GET("/ferris-wheel", r.customerPointsHandler.GetFerrisWheelGame) + customer.POST("/spin", r.spinGameHandler.PlaySpinGame) } organizations := v1.Group("/organizations") @@ -574,6 +578,17 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { campaigns.PUT("/:id", r.campaignHandler.UpdateCampaign) campaigns.DELETE("/:id", r.campaignHandler.DeleteCampaign) } + + // Campaign Rules + campaignRules := gamification.Group("/campaign-rules") + { + campaignRules.POST("", r.campaignHandler.CreateCampaignRule) + campaignRules.GET("", r.campaignHandler.ListCampaignRules) + campaignRules.GET("/:id", r.campaignHandler.GetCampaignRule) + campaignRules.PUT("/:id", r.campaignHandler.UpdateCampaignRule) + campaignRules.DELETE("/:id", r.campaignHandler.DeleteCampaignRule) + campaignRules.GET("/campaign/:campaign_id", r.campaignHandler.GetCampaignRulesByCampaignID) + } } outlets := protected.Group("/outlets") diff --git a/internal/service/campaign_service.go b/internal/service/campaign_service.go index 6fb4bcf..95a671a 100644 --- a/internal/service/campaign_service.go +++ b/internal/service/campaign_service.go @@ -18,15 +18,24 @@ type CampaignService interface { DeleteCampaign(ctx context.Context, id string) error GetActiveCampaigns(ctx context.Context) ([]models.CampaignResponse, error) GetCampaignsForApp(ctx context.Context) ([]models.CampaignResponse, error) + // Campaign Rules methods + CreateCampaignRule(ctx context.Context, req *contract.CreateCampaignRuleRequest) (*models.CampaignRuleResponse, error) + GetCampaignRule(ctx context.Context, id string) (*models.CampaignRuleResponse, error) + ListCampaignRules(ctx context.Context, req *contract.ListCampaignRulesRequest) (*models.ListCampaignRulesResponse, error) + UpdateCampaignRule(ctx context.Context, req *contract.UpdateCampaignRuleRequest) (*models.CampaignRuleResponse, error) + DeleteCampaignRule(ctx context.Context, id string) error + GetCampaignRulesByCampaignID(ctx context.Context, campaignID string) ([]models.CampaignRuleResponse, error) } type campaignService struct { - campaignProcessor processor.CampaignProcessor + campaignProcessor processor.CampaignProcessor + campaignRuleProcessor processor.CampaignRuleProcessor } -func NewCampaignService(campaignProcessor processor.CampaignProcessor) CampaignService { +func NewCampaignService(campaignProcessor processor.CampaignProcessor, campaignRuleProcessor processor.CampaignRuleProcessor) CampaignService { return &campaignService{ - campaignProcessor: campaignProcessor, + campaignProcessor: campaignProcessor, + campaignRuleProcessor: campaignRuleProcessor, } } @@ -41,11 +50,6 @@ func (s *campaignService) CreateCampaign(ctx context.Context, req *contract.Crea return nil, fmt.Errorf("validation failed: %w", err) } - // Validate rules - if err := s.validateCampaignRules(req.Rules); err != nil { - return nil, fmt.Errorf("validation failed: %w", err) - } - // Validate position if req.Position < 0 { return nil, fmt.Errorf("position cannot be negative") @@ -115,11 +119,6 @@ func (s *campaignService) UpdateCampaign(ctx context.Context, req *contract.Upda return nil, fmt.Errorf("validation failed: %w", err) } - // Validate rules - if err := s.validateCampaignRules(req.Rules); err != nil { - return nil, fmt.Errorf("validation failed: %w", err) - } - // Validate position if req.Position < 0 { return nil, fmt.Errorf("position cannot be negative") @@ -164,6 +163,137 @@ func (s *campaignService) GetCampaignsForApp(ctx context.Context) ([]models.Camp return campaigns, nil } +// Campaign Rules methods + +func (s *campaignService) CreateCampaignRule(ctx context.Context, req *contract.CreateCampaignRuleRequest) (*models.CampaignRuleResponse, error) { + // Validate rule type + if err := s.validateRuleType(req.RuleType); err != nil { + return nil, fmt.Errorf("validation failed: %w", err) + } + + // Validate reward type + if err := s.validateRewardType(req.RewardType); err != nil { + return nil, fmt.Errorf("validation failed: %w", err) + } + + // Validate reward value based on reward type + if req.RewardType == "POINTS" || req.RewardType == "TOKENS" { + if req.RewardValue == nil || *req.RewardValue <= 0 { + return nil, fmt.Errorf("reward value must be positive for %s type", req.RewardType) + } + } + + // Validate reward reference ID for REWARD type + if req.RewardType == "REWARD" { + if req.RewardRefID == nil { + return nil, fmt.Errorf("reward reference ID is required for REWARD type") + } + } + + response, err := s.campaignRuleProcessor.CreateCampaignRule(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to create campaign rule: %w", err) + } + + return response, nil +} + +func (s *campaignService) GetCampaignRule(ctx context.Context, id string) (*models.CampaignRuleResponse, error) { + if id == "" { + return nil, fmt.Errorf("campaign rule ID is required") + } + + response, err := s.campaignRuleProcessor.GetCampaignRule(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get campaign rule: %w", err) + } + + return response, nil +} + +func (s *campaignService) ListCampaignRules(ctx context.Context, req *contract.ListCampaignRulesRequest) (*models.ListCampaignRulesResponse, error) { + // Set default pagination + if req.Page <= 0 { + req.Page = 1 + } + if req.Limit <= 0 { + req.Limit = 10 + } + if req.Limit > 100 { + req.Limit = 100 + } + + response, err := s.campaignRuleProcessor.ListCampaignRules(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to list campaign rules: %w", err) + } + + return response, nil +} + +func (s *campaignService) UpdateCampaignRule(ctx context.Context, req *contract.UpdateCampaignRuleRequest) (*models.CampaignRuleResponse, error) { + if req.ID.String() == "" { + return nil, fmt.Errorf("campaign rule ID is required") + } + + // Validate rule type + if err := s.validateRuleType(req.RuleType); err != nil { + return nil, fmt.Errorf("validation failed: %w", err) + } + + // Validate reward type + if err := s.validateRewardType(req.RewardType); err != nil { + return nil, fmt.Errorf("validation failed: %w", err) + } + + // Validate reward value based on reward type + if req.RewardType == "POINTS" || req.RewardType == "TOKENS" { + if req.RewardValue == nil || *req.RewardValue <= 0 { + return nil, fmt.Errorf("reward value must be positive for %s type", req.RewardType) + } + } + + // Validate reward reference ID for REWARD type + if req.RewardType == "REWARD" { + if req.RewardRefID == nil { + return nil, fmt.Errorf("reward reference ID is required for REWARD type") + } + } + + response, err := s.campaignRuleProcessor.UpdateCampaignRule(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to update campaign rule: %w", err) + } + + return response, nil +} + +func (s *campaignService) DeleteCampaignRule(ctx context.Context, id string) error { + if id == "" { + return fmt.Errorf("campaign rule ID is required") + } + + err := s.campaignRuleProcessor.DeleteCampaignRule(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete campaign rule: %w", err) + } + + return nil +} + +func (s *campaignService) GetCampaignRulesByCampaignID(ctx context.Context, campaignID string) ([]models.CampaignRuleResponse, error) { + if campaignID == "" { + return nil, fmt.Errorf("campaign ID is required") + } + + rules, err := s.campaignRuleProcessor.GetCampaignRulesByCampaignID(ctx, campaignID) + if err != nil { + return nil, fmt.Errorf("failed to get campaign rules by campaign ID: %w", err) + } + + return rules, nil +} + func (s *campaignService) validateCampaignType(campaignType string) error { validTypes := []string{"REWARD", "POINTS", "TOKENS", "MIXED"} for _, validType := range validTypes { diff --git a/internal/service/spin_game_service.go b/internal/service/spin_game_service.go new file mode 100644 index 0000000..dcfd93e --- /dev/null +++ b/internal/service/spin_game_service.go @@ -0,0 +1,98 @@ +package service + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/repository" + + "github.com/google/uuid" +) + +type SpinGameService interface { + PlaySpinGame(ctx context.Context, customerID string, req *models.SpinGameRequest) (*models.SpinGameResponse, error) +} + +type spinGameService struct { + gamePlayProcessor *processor.GamePlayProcessor + txManager *repository.TxManager +} + +func NewSpinGameService(gamePlayProcessor *processor.GamePlayProcessor, txManager *repository.TxManager) SpinGameService { + return &spinGameService{ + gamePlayProcessor: gamePlayProcessor, + txManager: txManager, + } +} + +func (s *spinGameService) PlaySpinGame(ctx context.Context, customerID string, req *models.SpinGameRequest) (*models.SpinGameResponse, error) { + // Validate customer ID + if customerID == "" { + return nil, fmt.Errorf("customer ID is required") + } + + // Parse spin ID to UUID + spinID, err := uuid.Parse(req.SpinID) + if err != nil { + return nil, fmt.Errorf("invalid spin ID format: %w", err) + } + + // Parse customer ID to UUID + customerUUID, err := uuid.Parse(customerID) + if err != nil { + return nil, fmt.Errorf("invalid customer ID format: %w", err) + } + + // Use transaction to ensure consistency + var playResponse *models.PlayGameResponse + err = s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + // Create play game request + playGameReq := &models.PlayGameRequest{ + GameID: spinID, + CustomerID: customerUUID, + TokenUsed: 1, // Default to 1 token per spin + } + + // Play the game within transaction + var playErr error + playResponse, playErr = s.gamePlayProcessor.PlayGame(txCtx, playGameReq) + if playErr != nil { + return fmt.Errorf("failed to play spin game: %w", playErr) + } + + return nil + }) + + if err != nil { + return nil, err + } + + // Convert prize to customer-facing format + var customerPrize *models.CustomerGamePrizeResponse + if playResponse.PrizeWon != nil { + customerPrize = &models.CustomerGamePrizeResponse{ + ID: playResponse.PrizeWon.ID, + GameID: playResponse.PrizeWon.GameID, + Name: playResponse.PrizeWon.Name, + Image: playResponse.PrizeWon.Image, + Metadata: &playResponse.PrizeWon.Metadata, + CreatedAt: playResponse.PrizeWon.CreatedAt, + UpdatedAt: playResponse.PrizeWon.UpdatedAt, + } + } + + // Convert to spin game response + response := &models.SpinGameResponse{ + Status: "SUCCESS", + Message: "Spin game completed successfully.", + Data: &models.SpinGameResponseData{ + GamePlay: playResponse.GamePlay, + PrizeWon: customerPrize, + TokensRemaining: playResponse.TokensRemaining, + }, + } + + return response, nil +} diff --git a/internal/validator/campaign_validator.go b/internal/validator/campaign_validator.go index 1658e8f..d46bae7 100644 --- a/internal/validator/campaign_validator.go +++ b/internal/validator/campaign_validator.go @@ -56,17 +56,6 @@ func (v *CampaignValidatorImpl) ValidateCreateCampaignRequest(req *contract.Crea return errors.New("position cannot be negative"), constants.ValidationErrorCode } - // Validate rules - if len(req.Rules) == 0 { - return errors.New("at least one rule is required"), constants.ValidationErrorCode - } - - for i, rule := range req.Rules { - if err := v.validateCampaignRule(&rule, i+1); err != nil { - return err, constants.ValidationErrorCode - } - } - return nil, "" } @@ -103,17 +92,6 @@ func (v *CampaignValidatorImpl) ValidateUpdateCampaignRequest(req *contract.Upda return errors.New("position cannot be negative"), constants.ValidationErrorCode } - // Validate rules - if len(req.Rules) == 0 { - return errors.New("at least one rule is required"), constants.ValidationErrorCode - } - - for i, rule := range req.Rules { - if err := v.validateCampaignRule(&rule, i+1); err != nil { - return err, constants.ValidationErrorCode - } - } - return nil, "" }