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] }