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
@@ -78,6 +78,20 @@ func (h *EnakGameCustomerHandler) GetSession(c *gin.Context) {
util.HandleResponse(c.Writer, c.Request, h.service.GetSession(c.Request.Context(), customerID, id), method)
}
// Leaderboard is GET /customer/enakgame/leaderboard?date=&game_id=&limit=.
func (h *EnakGameCustomerHandler) Leaderboard(c *gin.Context) {
const method = "EnakGameCustomerHandler::Leaderboard"
customerID, ok := customerIDFromGin(c, method)
if !ok {
return
}
var q models.EnakGameLeaderboardQuery
if !bindQuery(c, &q, method) {
return
}
util.HandleResponse(c.Writer, c.Request, h.service.Leaderboard(c.Request.Context(), customerID, q), method)
}
// CompleteSession is POST /customer/enakgame/sessions/:id/complete. The body is read
// leniently on purpose: a reward amount, or anything else the backend does not take
// from the client, is ignored rather than refused (P1). An empty body is a result