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 }