4aa6174968
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
Add the "Кошелёк" section to the settings hub: context-visible chip balances, active benefits (no-ads term/forever, hints) and a storefront of chip-priced values and money-priced chip packs. Guests have no wallet; the Google Play build hides the money purchases behind a RuStore stub; a web purchase that would draw VK/Telegram chips warns first. Add the catalog read path the storefront needs — a context-projected GET /api/v1/user/wallet/catalog (payments service + store, gateway op wallet.catalog, FBS Catalog/CatalogProduct/CatalogAtom, client decode) — plus the client leg for the existing wallet.get/buy ops. Value spends reuse the existing spend path; the chip-pack purchase (money order flow) arrives with payment intake, so its action is a disabled placeholder for now. Covered by Go unit (catalog projection) + integration (/wallet/catalog over Postgres), vitest (formatting, spendable selection, web-spend warning, GP flag, codec + gateway encode round-trips) and Playwright mock e2e (render, guest-hidden, GP stub, warning) on Chromium + WebKit.
83 lines
3.2 KiB
Go
83 lines
3.2 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 {
|
|
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
|
|
}
|