240 lines
6.5 KiB
Go
240 lines
6.5 KiB
Go
|
|
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]
|
||
|
|
}
|