chore(catalog): order the admin catalog list like the public offer
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) Successful in 1m11s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s

Sales (chip packs) first, ascending by rouble price; then the chip-exchange
values, grouped and price-sorted the same way projectOfferPricing lists them, so
the /catalog console mirrors what a buyer sees. Internal cosmetics only — no
product behaviour change. The value-group order moves to a shared helper
(valueGroup) so the offer and the admin list cannot drift.
This commit is contained in:
Ilia Denisov
2026-07-11 12:53:37 +02:00
parent bb71e7b1c7
commit 2c2316fb5e
4 changed files with 120 additions and 5 deletions
@@ -1,9 +1,12 @@
package payments
import (
"cmp"
"context"
"errors"
"fmt"
"math"
"slices"
"strings"
"github.com/google/uuid"
@@ -147,6 +150,65 @@ func (s *Service) AdminCatalog(ctx context.Context) ([]AdminProduct, error) {
return s.store.adminCatalog(ctx)
}
// SortAdminCatalog orders an admin catalog list in place the same way the public offer lists products
// ([projectOfferPricing]): chip packs first, ascending by rouble price; then chip-priced values,
// grouped (hints only, no-ads only, no-ads + hints, tournament) and ascending by chip price within a
// group. It is stable, so equal-keyed products keep their incoming order, and it keys archived
// products the same as active ones (the admin list shows both).
func SortAdminCatalog(products []AdminProduct) {
slices.SortStableFunc(products, func(a, b AdminProduct) int {
if pa, pb := adminIsPack(a), adminIsPack(b); pa != pb {
if pa {
return -1 // packs (sales) before values (chip exchange)
}
return 1
} else if pa {
return cmp.Compare(adminPriceAmount(a, string(SourceDirect), CurrencyRUB), adminPriceAmount(b, string(SourceDirect), CurrencyRUB))
}
if d := cmp.Compare(adminValueGroup(a), adminValueGroup(b)); d != 0 {
return d
}
return cmp.Compare(adminPriceAmount(a, "", CurrencyChip), adminPriceAmount(b, "", CurrencyChip))
})
}
// adminIsPack reports whether the product is a chip pack (it carries the chips atom).
func adminIsPack(p AdminProduct) bool {
for _, a := range p.Atoms {
if a.Atom == atomChips {
return true
}
}
return false
}
// adminValueGroup ranks a chip-priced value into the shared listing groups (see [valueGroup]).
func adminValueGroup(p AdminProduct) int {
hasHints, hasNoAds, hasTournament := false, false, false
for _, a := range p.Atoms {
switch a.Atom {
case "hints":
hasHints = true
case "noads_days":
hasNoAds = true
case "tournament":
hasTournament = true
}
}
return valueGroup(hasHints, hasNoAds, hasTournament)
}
// adminPriceAmount returns the minor-unit amount of the product's price for the method and currency,
// or math.MaxInt64 when it carries no such price (so a misconfigured product sorts last, not first).
func adminPriceAmount(p AdminProduct, method string, cur Currency) int64 {
for _, pr := range p.Prices {
if pr.Method == method && pr.Currency == cur {
return pr.Amount
}
}
return math.MaxInt64
}
// CreateProduct validates and inserts a new product with its atoms and prices, returning its id. A
// product created active must satisfy the sellable shape.
func (s *Service) CreateProduct(ctx context.Context, in ProductInput, active bool) (uuid.UUID, error) {