feat(admin): admin grant — raw benefits and by-product reward bundles
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 26s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m48s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 26s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m48s
The /_gm user card gains a Grant panel: grant raw benefit atoms (hints / no-ads days / forever) or a defined value product (a reward bundle, including an archived one), origin-picked. Both write an admin_grant ledger row via payments.Grant / GrantProduct; the by-product grant records the source product_id + snapshot. Both refuse a chips atom (never grant currency) or a tournament atom (no credit target yet); chips/tournament products are also kept out of the by-product picker. Tests: the console grant end to end (raw, by-product, refuse a chips pack, CSRF-guarded).
This commit is contained in:
@@ -26,6 +26,14 @@ var (
|
||||
// ErrNotAValue means the product has no chip price (it is a chip pack or unpriced), so it
|
||||
// cannot be bought with chips.
|
||||
ErrNotAValue = errors.New("payments: product is not a chip-priced value")
|
||||
// ErrCannotGrantChips means an admin grant targeted a product carrying the chips atom — the
|
||||
// admin never grants currency (a gifted balance would bypass the cash desk, D16).
|
||||
ErrCannotGrantChips = errors.New("payments: cannot grant chips")
|
||||
// ErrCannotGrantTournament means an admin grant targeted a product carrying the tournament atom,
|
||||
// which has no credit target until the tournament stage.
|
||||
ErrCannotGrantTournament = errors.New("payments: cannot grant a tournament atom yet")
|
||||
// ErrNothingToGrant means the product's atoms yield no grantable benefit (hints / no-ads days).
|
||||
ErrNothingToGrant = errors.New("payments: product has nothing to grant")
|
||||
)
|
||||
|
||||
// withTx runs fn inside a transaction on db, rolling back on error or panic.
|
||||
@@ -312,6 +320,22 @@ func (s *Store) grant(ctx context.Context, accountID uuid.UUID, origin Source, d
|
||||
return nil
|
||||
}
|
||||
|
||||
// grantProduct is grant with the source product recorded on the ledger row (product_id), for an
|
||||
// admin grant-by-product — the benefit is the product's atoms, the ledger stays auditable to it.
|
||||
func (s *Store) grantProduct(ctx context.Context, accountID uuid.UUID, origin Source, productID uuid.UUID, d benefitDelta, snapshot []byte, now time.Time) error {
|
||||
err := withTx(ctx, s.db, func(tx *sql.Tx) error {
|
||||
if err := insertLedgerTx(ctx, tx, accountID, "admin_grant", nil, &origin, 0, &productID, nil, nil, nil, snapshot, now); err != nil {
|
||||
return err
|
||||
}
|
||||
return applyBenefitTx(ctx, tx, accountID, origin, d, now)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.cache.invalidate(accountID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// consumeHint decrements one hint from the first applicable origin (in the given priority order)
|
||||
// that has one, with a guarded update. It returns whether a hint was spent.
|
||||
func (s *Store) consumeHint(ctx context.Context, accountID uuid.UUID, origins []Source, now time.Time) (bool, error) {
|
||||
|
||||
Reference in New Issue
Block a user