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:
efrilm
2026-10-12 16:15:02 +07:00
co-authored by Claude Opus 5.5
parent c43baa53e1
commit 3af16a1fbc
10 changed files with 398 additions and 4 deletions
+6
View File
@@ -74,6 +74,7 @@ type EnakGameCustomerService interface {
CompleteSession(ctx context.Context, customerID, sessionID uuid.UUID, in models.GameSessionCompleteInput) *contract.Response
ListSessions(ctx context.Context, customerID uuid.UUID, q models.GameSessionListQuery) *contract.Response
GetSession(ctx context.Context, customerID, sessionID uuid.UUID) *contract.Response
Leaderboard(ctx context.Context, customerID uuid.UUID, q models.EnakGameLeaderboardQuery) *contract.Response
ListVouchers(ctx context.Context, customerID uuid.UUID) *contract.Response
RedeemVoucher(ctx context.Context, customerID, voucherID uuid.UUID, req *contract.RedeemVoucherRequest, idempotencyKey string, info models.CustomerPinRequestInfo) *contract.Response
@@ -381,6 +382,11 @@ func (s *EnakGameCustomerServiceImpl) GetSession(ctx context.Context, customerID
return respond(ctx, session, err)
}
func (s *EnakGameCustomerServiceImpl) Leaderboard(ctx context.Context, customerID uuid.UUID, q models.EnakGameLeaderboardQuery) *contract.Response {
board, err := s.sessions.Leaderboard(ctx, customerID, q)
return respond(ctx, board, err)
}
// decodeStrict reads a JSON body into v, refusing unknown fields and trailing data.
// It returns the error response, or nil when the body fits.
func decodeStrict(body []byte, v any) *contract.Response {