b6af682381
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 23s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m49s
The wallet storefront listed products in creation order — its purchases and its chip-exchange values were unsorted, unlike the admin console and the public offer. Extract the ordering (chip packs first by rouble price, then chip-priced values grouped hints → no-ads → combo → tournament and by chip price) into one comparator, compareCatalogRank over a small rank key, and apply it at all three sites (projectCatalog, projectOfferPricing, SortAdminCatalog) — so the subgroup ranking (valueGroup) and the full order now live in one place. The rewarded "watch for chips" CTA is a wallet-driven element rendered above the packs, so it is unaffected and stays on top. Also add the admin catalog active/all toggle: AdminCatalog takes includeInactive, the console shows active products by default and ?all=1 lists archived ones too (the detail and grant forms keep loading all products). Tests: a storefront canonical-order unit test (reproduced the unsorted bug first); an integration test for the active/all toggle. Existing offer/admin order tests unchanged — behaviour preserved.
88 lines
3.5 KiB
Go
88 lines
3.5 KiB
Go
package payments
|
|
|
|
// AtomQty is one atom line of a catalog product in the read model: the base value type it grants
|
|
// (chips / hints / no-ads days / tournament) and how many of it the product carries.
|
|
type AtomQty struct {
|
|
AtomType string
|
|
Quantity int
|
|
}
|
|
|
|
// CatalogProduct is one storefront product projected for the execution context. A value is
|
|
// bought with chips, so it carries Chips (its uniform chip price) and no money price. A chip pack
|
|
// funds chips with money, so it carries MoneyAmount (minor units) and MoneyCurrency for the
|
|
// context's payment method and no chip price. Atoms lists what the product grants.
|
|
type CatalogProduct struct {
|
|
Kind string // "value" (bought with chips) or "pack" (funds chips with money)
|
|
ProductID string
|
|
Title string
|
|
Chips int // a value's uniform chip price; 0 for a pack
|
|
MoneyAmount int64 // a pack's price in the context currency's minor units; 0 for a value
|
|
MoneyCurrency string // a pack's currency for the context method; empty for a value
|
|
Atoms []AtomQty
|
|
}
|
|
|
|
// CatalogView is the storefront read model for one execution context: the products a player sees
|
|
// and can buy there — every chip-priced value plus the chip packs priced in the context's method.
|
|
type CatalogView struct {
|
|
Products []CatalogProduct
|
|
}
|
|
|
|
// atomChips is the atom type that marks a product as a chip pack (it funds chips) rather than a
|
|
// value (bought with chips). A value never carries it.
|
|
const atomChips = "chips"
|
|
|
|
// projectCatalog turns the raw active catalog into the storefront for the context. A value (no
|
|
// chips atom) is shown everywhere at its CHIP price. A chip pack is shown only when it carries a
|
|
// price for the context's payment method (cxt.Kind: vk / telegram / direct), priced in that
|
|
// method's currency. A value missing its CHIP price and a pack missing a price for the context
|
|
// method are omitted (misconfigured or unavailable there). An untrusted context (empty Kind) has
|
|
// no method, so only values show; buying is gated server-side regardless.
|
|
func projectCatalog(entries []catalogEntry, cxt Context) CatalogView {
|
|
// Present products in the canonical listing order shared with the offer and the admin console
|
|
// (packs first by rouble price, then values grouped hints → no-ads → combo and by chip price); the
|
|
// client renders them as received. Ordered in place — the caller passes a fresh loadCatalog result
|
|
// it does not reuse.
|
|
sortCatalogEntries(entries)
|
|
var out CatalogView
|
|
for _, e := range entries {
|
|
isPack := false
|
|
atoms := make([]AtomQty, 0, len(e.atoms))
|
|
for _, a := range e.atoms {
|
|
atoms = append(atoms, AtomQty{AtomType: a.atomType, Quantity: a.quantity})
|
|
if a.atomType == atomChips {
|
|
isPack = true
|
|
}
|
|
}
|
|
p := CatalogProduct{ProductID: e.id.String(), Title: e.title, Atoms: atoms}
|
|
if isPack {
|
|
pr, ok := findPrice(e.prices, string(cxt.Kind))
|
|
if !ok {
|
|
continue // no price for this context's method — not purchasable here
|
|
}
|
|
p.Kind = "pack"
|
|
p.MoneyAmount = pr.amount
|
|
p.MoneyCurrency = string(pr.currency)
|
|
} else {
|
|
pr, ok := findPrice(e.prices, "")
|
|
if !ok {
|
|
continue // a value must carry a CHIP price
|
|
}
|
|
p.Kind = "value"
|
|
p.Chips = int(pr.amount)
|
|
}
|
|
out.Products = append(out.Products, p)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// findPrice returns the price row for the given payment method — the empty string selects a
|
|
// value's CHIP price row (stored with a NULL method).
|
|
func findPrice(prices []priceRow, method string) (priceRow, bool) {
|
|
for _, pr := range prices {
|
|
if pr.method == method {
|
|
return pr, true
|
|
}
|
|
}
|
|
return priceRow{}, false
|
|
}
|