feat(enakgame): filter play history by game and status

GET /customer/enakgame/sessions takes optional game_id and status, so a game
reloaded mid-play finds the session it was running (status=STARTED) instead
of starting a new one and charging EnakCoin again. An invalid game_id or
status is refused.

integration-enakgame.md §4.4 now describes recovery after a reload: keep the
session_id in sessionStorage, continue a STARTED session before expires_at,
and call complete again for a COMPLETED one to get the full answer, prize
included. The mobile guide and RFC §11 mention the filters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
efrilm
2026-10-08 12:51:39 +07:00
co-authored by Claude Opus 5.5
parent b5d2cd491a
commit 52e8fe11c6
10 changed files with 129 additions and 24 deletions
@@ -287,7 +287,7 @@ func TestGameSessionRepository_ReadsAndMoves(t *testing.T) {
assert.True(t, got.Flagged)
assert.NotNil(t, got.CompletionFailedAt)
sessions, total, err := f.sessions.ListCustomerSessions(ctx, f.customer, 0, 1)
sessions, total, err := f.sessions.ListCustomerSessions(ctx, CustomerSessionFilter{CustomerID: f.customer, Limit: 1})
require.NoError(t, err)
assert.EqualValues(t, 2, total)
assert.Len(t, sessions, 1)
+21 -3
View File
@@ -50,7 +50,7 @@ type GameSessionRepository interface {
GetSessionBySpendTransaction(ctx context.Context, spendTransactionID uuid.UUID) (*entities.GameSession, error)
// ListCustomerSessions returns a page of a customer's sessions, newest first, and
// the total.
ListCustomerSessions(ctx context.Context, customerID uuid.UUID, offset, limit int) ([]entities.GameSession, int64, error)
ListCustomerSessions(ctx context.Context, filter CustomerSessionFilter) ([]entities.GameSession, int64, error)
CompleteSession(ctx context.Context, id uuid.UUID, completion GameSessionCompletion) (bool, error)
RefundSession(ctx context.Context, id, refundTransactionID uuid.UUID, reason string, endedAt time.Time) (bool, error)
@@ -116,8 +116,26 @@ func (r *gameSessionRepository) GetSessionBySpendTransaction(ctx context.Context
return r.first(DBFromContext(ctx, r.db).WithContext(ctx).Where("spend_transaction_id = ?", spendTransactionID))
}
func (r *gameSessionRepository) ListCustomerSessions(ctx context.Context, customerID uuid.UUID, offset, limit int) ([]entities.GameSession, int64, error) {
q := DBFromContext(ctx, r.db).WithContext(ctx).Model(&entities.GameSession{}).Where("customer_id = ?", customerID)
// CustomerSessionFilter selects a customer's sessions.
type CustomerSessionFilter struct {
CustomerID uuid.UUID
// Nil for every game.
GameID *uuid.UUID
// Empty for every status.
Status string
Offset int
Limit int
}
func (r *gameSessionRepository) ListCustomerSessions(ctx context.Context, filter CustomerSessionFilter) ([]entities.GameSession, int64, error) {
q := DBFromContext(ctx, r.db).WithContext(ctx).Model(&entities.GameSession{}).Where("customer_id = ?", filter.CustomerID)
if filter.GameID != nil {
q = q.Where("game_id = ?", *filter.GameID)
}
if filter.Status != "" {
q = q.Where("status = ?", filter.Status)
}
offset, limit := filter.Offset, filter.Limit
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count game sessions: %w", err)