feat(enakgame): weekly leaderboard
GET /customer/enakgame/leaderboard ranks the customers of an organization by the EnakCoin they won from games in one week, Monday to Sunday in Asia/Jakarta. Only completed, unflagged sessions count, by the week they started; entry cost is not subtracted. Equal totals rank by who reached theirs first, so no two customers share a rank. Optional date (any day of the week, this week by default), game_id and limit (10, at most 100). The response holds the top ranks and, under me, the customer asking wherever they rank. Other customers are shown by a masked name: the first word whole, the others by their first letter; a word with a digit keeps only its first character. The EnakGame web game shows it: integration-enakgame.md §10 (checklist now §11) and RFC §11. The DB test needs TEST_DATABASE_URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c43baa53e1
commit
3af16a1fbc
@@ -70,6 +70,32 @@ type GameSessionRepository interface {
|
||||
|
||||
CreateSessionRewards(ctx context.Context, rewards []entities.GameSessionReward) error
|
||||
ListSessionRewards(ctx context.Context, sessionID uuid.UUID) ([]entities.GameSessionReward, error)
|
||||
|
||||
// Leaderboard ranks the active customers of an organization by the EnakCoin their
|
||||
// completed, unflagged sessions started in [from, to) won, and returns the first
|
||||
// limit ranks plus the row of filter.CustomerID wherever it ranks.
|
||||
Leaderboard(ctx context.Context, filter LeaderboardFilter) ([]LeaderboardRow, error)
|
||||
}
|
||||
|
||||
// LeaderboardFilter selects the sessions a leaderboard counts.
|
||||
type LeaderboardFilter struct {
|
||||
OrganizationID uuid.UUID
|
||||
// Nil for every game.
|
||||
GameID *uuid.UUID
|
||||
From time.Time
|
||||
To time.Time
|
||||
Limit int
|
||||
// The customer asking, returned even below Limit.
|
||||
CustomerID uuid.UUID
|
||||
}
|
||||
|
||||
// LeaderboardRow is one customer's total. Equal totals rank by who reached theirs
|
||||
// first, so no two rows share a rank.
|
||||
type LeaderboardRow struct {
|
||||
Rank int
|
||||
CustomerID uuid.UUID
|
||||
Name string
|
||||
Coins int64
|
||||
}
|
||||
|
||||
type gameSessionRepository struct {
|
||||
@@ -255,3 +281,45 @@ func (r *gameSessionRepository) ListDueSessions(ctx context.Context, now time.Ti
|
||||
}
|
||||
return due, nil
|
||||
}
|
||||
|
||||
func (r *gameSessionRepository) Leaderboard(ctx context.Context, filter LeaderboardFilter) ([]LeaderboardRow, error) {
|
||||
gameFilter, args := "", []any{filter.OrganizationID, filter.From, filter.To, constants.GameSessionStatusCompleted}
|
||||
if filter.GameID != nil {
|
||||
gameFilter = "AND s.game_id = ?"
|
||||
args = append(args, *filter.GameID)
|
||||
}
|
||||
args = append(args, filter.Limit, filter.CustomerID)
|
||||
var rows []struct {
|
||||
Rank int
|
||||
CustomerID string
|
||||
Name string
|
||||
Coins int64
|
||||
}
|
||||
err := DBFromContext(ctx, r.db).WithContext(ctx).Raw(`
|
||||
WITH totals AS (
|
||||
SELECT s.customer_id, SUM(s.reward_total) AS coins, MAX(s.ended_at) AS reached_at
|
||||
FROM game_sessions s
|
||||
WHERE s.organization_id = ? AND s.started_at >= ? AND s.started_at < ?
|
||||
AND s.status = ? AND NOT s.flagged AND s.reward_total > 0 `+gameFilter+`
|
||||
GROUP BY s.customer_id
|
||||
), ranked AS (
|
||||
SELECT t.customer_id, c.name, t.coins,
|
||||
ROW_NUMBER() OVER (ORDER BY t.coins DESC, t.reached_at, t.customer_id) AS rank
|
||||
FROM totals t
|
||||
JOIN customers c ON c.id = t.customer_id AND c.is_active
|
||||
)
|
||||
SELECT rank, customer_id::text AS customer_id, name, coins
|
||||
FROM ranked
|
||||
WHERE rank <= ? OR customer_id = ?
|
||||
ORDER BY rank`, args...).Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read game leaderboard: %w", err)
|
||||
}
|
||||
out := make([]LeaderboardRow, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
l := LeaderboardRow{Rank: row.Rank, Name: row.Name, Coins: row.Coins}
|
||||
l.CustomerID, _ = uuid.Parse(row.CustomerID)
|
||||
out = append(out, l)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user