package processor import ( "context" "strings" "time" "github.com/google/uuid" "apskel-pos-be/internal/models" "apskel-pos-be/internal/repository" ) const ( // leaderboardLimit is how many ranks a leaderboard shows unless asked, and // leaderboardMaxLimit the most it shows. leaderboardLimit = 10 leaderboardMaxLimit = 100 ) // Leaderboard ranks the customers of the customer's organization by the EnakCoin they // won from games in one week, Monday to Sunday in Asia/Jakarta: this week, or the one // holding q.Date. A session counts in the week it started. Refunded, expired and // flagged sessions count for nothing. func (p *GameSessionProcessor) Leaderboard(ctx context.Context, customerID uuid.UUID, q models.EnakGameLeaderboardQuery) (*models.EnakGameLeaderboard, error) { day := walletDay(p.now()) if s := strings.TrimSpace(q.Date); s != "" { d, err := time.ParseInLocation("2006-01-02", s, walletDisplayLocation) if err != nil { return nil, gameSessionRejected("date must be YYYY-MM-DD") } day = d } weekStart := leaderboardWeekStart(day) filter := repository.LeaderboardFilter{ CustomerID: customerID, From: weekStart, To: weekStart.AddDate(0, 0, 7), Limit: q.Limit, } if filter.Limit < 1 || filter.Limit > leaderboardMaxLimit { filter.Limit = leaderboardLimit } if s := strings.TrimSpace(q.GameID); s != "" { id, err := uuid.Parse(s) if err != nil { return nil, gameSessionRejected("game_id must be a UUID") } filter.GameID = &id } customer, err := p.customers.GetCustomer(ctx, customerID) if err != nil { return nil, err } filter.OrganizationID = customer.OrganizationID rows, err := p.sessions.Leaderboard(ctx, filter) if err != nil { return nil, err } board := &models.EnakGameLeaderboard{ WeekStart: weekStart.Format("2006-01-02"), WeekEnd: weekStart.AddDate(0, 0, 6).Format("2006-01-02"), GameID: filter.GameID, Entries: []models.EnakGameLeaderboardEntry{}, } for _, row := range rows { entry := models.EnakGameLeaderboardEntry{ Rank: row.Rank, Name: maskCustomerName(row.Name), Coins: row.Coins, IsMe: row.CustomerID == customerID, } if row.Rank <= filter.Limit { board.Entries = append(board.Entries, entry) } if entry.IsMe { me := entry board.Me = &me } } return board, nil } // leaderboardWeekStart is the Monday, at midnight in Asia/Jakarta, of the week of day. func leaderboardWeekStart(day time.Time) time.Time { day = walletDay(day) return day.AddDate(0, 0, -((int(day.Weekday()) + 6) % 7)) } // maskCustomerName keeps the first word of a name and the first letter of every other // word: "Budi Santoso" is shown as "Budi S******". A word with a digit, as when a phone // number was used for the name, keeps only its first character, the first word too. func maskCustomerName(name string) string { words := strings.Fields(name) if len(words) == 0 { return "Pemain" } for i, w := range words { if i == 0 && !strings.ContainsAny(w, "0123456789") { continue } r := []rune(w) words[i] = string(r[0]) + strings.Repeat("*", len(r)-1) } return strings.Join(words, " ") }