feat(admin): grant hints to a user's wallet from the console
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m5s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m5s
Add an "Add hints" form on the admin user card that additively tops up a player's hint wallet (1-100 per grant). The grant is raise-only by construction (an additive UPDATE never lowers the balance) and stays correct under a concurrent in-game spend; a per-grant cap bounds a fat-finger, since the console can never reduce a wallet. The in-game hint policy is unchanged and already correct: a game offers the per-seat allowance plus the wallet, spending the allowance first and the wallet only after (covered by TestHintPolicy).
This commit is contained in:
@@ -430,6 +430,30 @@ func (s *Store) SpendHint(ctx context.Context, id uuid.UUID) (bool, error) {
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
// GrantHints adds n hints to the account's wallet and returns the new balance. n must be
|
||||
// positive: the additive update can only raise the balance, never lower it, so it enforces the
|
||||
// admin console's raise-only rule by construction and stays correct under a concurrent SpendHint.
|
||||
// It returns ErrNotFound when no account matches.
|
||||
func (s *Store) GrantHints(ctx context.Context, id uuid.UUID, n int) (int, error) {
|
||||
if n <= 0 {
|
||||
return 0, fmt.Errorf("account: grant hints %s: n must be positive, got %d", id, n)
|
||||
}
|
||||
stmt := table.Accounts.
|
||||
UPDATE(table.Accounts.HintBalance, table.Accounts.UpdatedAt).
|
||||
SET(table.Accounts.HintBalance.ADD(postgres.Int(int64(n))), postgres.TimestampzT(time.Now().UTC())).
|
||||
WHERE(table.Accounts.AccountID.EQ(postgres.UUID(id))).
|
||||
RETURNING(table.Accounts.HintBalance)
|
||||
|
||||
var row model.Accounts
|
||||
if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
|
||||
if errors.Is(err, qrm.ErrNoRows) {
|
||||
return 0, ErrNotFound
|
||||
}
|
||||
return 0, fmt.Errorf("account: grant hints %s: %w", id, err)
|
||||
}
|
||||
return int(row.HintBalance), nil
|
||||
}
|
||||
|
||||
// FlagHighRate stamps the soft "suspected high-rate" marker with at, only when
|
||||
// the account is not already flagged — the first sustained episode wins, and a
|
||||
// re-flag after an operator clear starts a fresh timestamp. An infra marker, not
|
||||
|
||||
Reference in New Issue
Block a user