Files
apskel-pos-backend/internal/repository/game_play_repository.go
T
efrilmandClaude Opus 5.5 a18bb072f5 feat(loyalty): pay every game with EnakCoin
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>
2026-09-30 12:20:42 +07:00

171 lines
4.7 KiB
Go

package repository
import (
"context"
"fmt"
"apskel-pos-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
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 (r *gamePlayRepository) Create(ctx context.Context, gamePlay *entities.GamePlay) error {
if err := DBFromContext(ctx, 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) {
var gamePlay entities.GamePlay
err := DBFromContext(ctx, r.db).WithContext(ctx).
Preload("Game").
Preload("Customer").
Preload("Prize").
First(&gamePlay, "id = ?", id).Error
if err != nil {
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
var total int64
query := DBFromContext(ctx, r.db).WithContext(ctx).Model(&entities.GamePlay{})
// Apply filters
if search != "" {
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)
}
// 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 == "desc" {
query = query.Order(fmt.Sprintf("%s DESC", sortBy))
} else {
query = query.Order(fmt.Sprintf("%s ASC", sortBy))
}
} else {
query = query.Order("created_at DESC")
}
// 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, fmt.Errorf("failed to list game plays: %w", err)
}
return gamePlays, total, nil
}
func (r *gamePlayRepository) GetByCustomerID(ctx context.Context, customerID uuid.UUID, limit int) ([]*entities.GamePlay, error) {
var gamePlays []*entities.GamePlay
query := DBFromContext(ctx, r.db).WithContext(ctx).
Where("customer_id = ?", customerID).
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) GetByGameID(ctx context.Context, gameID uuid.UUID, limit int) ([]*entities.GamePlay, error) {
var gamePlays []*entities.GamePlay
query := DBFromContext(ctx, r.db).WithContext(ctx).
Where("game_id = ?", gameID).
Preload("Customer").
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 game ID: %w", err)
}
return gamePlays, nil
}
func (r *gamePlayRepository) CountByCustomerID(ctx context.Context, customerID uuid.UUID) (int64, error) {
var count int64
err := DBFromContext(ctx, 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 := DBFromContext(ctx, 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
}