Games now spend the wallet's EnakCoin instead of the per-type tokens (docs/prd-point-coin.md F8, K1, PC-403). GamePlayProcessor.PlayGame charges the game's metadata.coin_cost, 1 when it is not set; a cost that is not a whole number of at least 1 refuses the game. In one transaction it picks the prize, takes the EnakCoin with a GAME_SPEND row pointing at the new game_plays.id (which locks the wallet, so a customer's plays at the same time queue up), records the play and takes the prize from stock. The play owns its transaction, so the spin service no longer wraps it, and the admin play endpoint is now atomic too. The game, game prize and game play repositories go through DBFromContext so they join that transaction. DecreaseStock now reports a prize that ran out (ErrGamePrizeOutOfStock) instead of silently updating nothing; that, or any other stock failure, cancels the whole play, where it used to be only printed. The manual AddTokens rollback is gone. Not enough EnakCoin, an inactive game or a prize that ran out answer 400 on /customer/spin instead of 500. game_plays.token_used is renamed coins_used (migration 000095). What a play costs is no longer the caller's choice, so PlayGameRequest loses token_used. Responses carry coins_used and coins_remaining; token_used and tokens_remaining stay as deprecated copies until the apps move over, and sort_by=token_used still sorts by coins_used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
81 lines
2.2 KiB
Go
81 lines
2.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"apskel-pos-be/internal/models"
|
|
"apskel-pos-be/internal/processor"
|
|
|
|
"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
|
|
}
|
|
|
|
func NewSpinGameService(gamePlayProcessor *processor.GamePlayProcessor) SpinGameService {
|
|
return &spinGameService{
|
|
gamePlayProcessor: gamePlayProcessor,
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// PlayGame runs its own transaction and charges the game's coin_cost in EnakCoin.
|
|
playResponse, err := s.gamePlayProcessor.PlayGame(ctx, &models.PlayGameRequest{
|
|
GameID: spinID,
|
|
CustomerID: customerUUID,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to play spin game: %w", 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,
|
|
CoinsRemaining: playResponse.CoinsRemaining,
|
|
},
|
|
}
|
|
|
|
return response, nil
|
|
}
|