apskel-pos-backend/internal/processor/game_play_processor.go

240 lines
6.5 KiB
Go
Raw Normal View History

2025-09-17 19:30:17 +07:00
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 {
2025-09-18 13:39:37 +07:00
gamePlayRepo repository.GamePlayRepository
2025-09-17 19:30:17 +07:00
gameRepo *repository.GameRepository
gamePrizeRepo *repository.GamePrizeRepository
customerTokensRepo *repository.CustomerTokensRepository
2025-09-18 12:01:20 +07:00
customerPointsRepo repository.CustomerPointsRepository
2025-09-17 19:30:17 +07:00
}
func NewGamePlayProcessor(
2025-09-18 13:39:37 +07:00
gamePlayRepo repository.GamePlayRepository,
2025-09-17 19:30:17 +07:00
gameRepo *repository.GameRepository,
gamePrizeRepo *repository.GamePrizeRepository,
customerTokensRepo *repository.CustomerTokensRepository,
2025-09-18 12:01:20 +07:00
customerPointsRepo repository.CustomerPointsRepository,
2025-09-17 19:30:17 +07:00
) *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
2025-09-18 13:39:37 +07:00
responses := mappers.ToGamePlayResponsesFromPointers(gamePlays)
2025-09-17 19:30:17 +07:00
// 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]
}