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.
90 lines
2.8 KiB
Go
90 lines
2.8 KiB
Go
package payments
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/go-jet/jet/v2/postgres"
|
|
"github.com/google/uuid"
|
|
|
|
"scrabble/backend/internal/postgres/jet/payments/model"
|
|
"scrabble/backend/internal/postgres/jet/payments/table"
|
|
)
|
|
|
|
// atomQty is one atom line of a product as loaded from the catalog: the atom type and quantity.
|
|
type atomQty struct {
|
|
atomType string
|
|
quantity int
|
|
}
|
|
|
|
// priceRow is one price of a product as loaded from the catalog: the payment method (empty for a
|
|
// value's CHIP price, stored with a NULL method) and the amount in that currency's minor units.
|
|
type priceRow struct {
|
|
method string
|
|
currency Currency
|
|
amount int64
|
|
}
|
|
|
|
// catalogEntry is a raw active product loaded for the storefront: its atom composition and every
|
|
// price row. [projectCatalog] turns it into the context-visible [CatalogProduct].
|
|
type catalogEntry struct {
|
|
id uuid.UUID
|
|
title string
|
|
atoms []atomQty
|
|
prices []priceRow
|
|
}
|
|
|
|
// loadCatalog reads every active product with its atoms and prices, ordered by creation, straight
|
|
// from the catalog tables. The catalog is small and rarely edited (admin console only), so it is
|
|
// read uncached — unlike the per-account balances/benefits the read cache fronts.
|
|
func (s *Store) loadCatalog(ctx context.Context) ([]catalogEntry, error) {
|
|
var prods []model.Product
|
|
if err := postgres.SELECT(table.Product.AllColumns).
|
|
FROM(table.Product).
|
|
WHERE(table.Product.Active.IS_TRUE()).
|
|
ORDER_BY(table.Product.CreatedAt.ASC()).
|
|
QueryContext(ctx, s.db, &prods); err != nil {
|
|
return nil, fmt.Errorf("payments: load catalog products: %w", err)
|
|
}
|
|
if len(prods) == 0 {
|
|
return nil, nil
|
|
}
|
|
entries := make([]catalogEntry, len(prods))
|
|
index := make(map[uuid.UUID]int, len(prods))
|
|
for i, p := range prods {
|
|
entries[i] = catalogEntry{id: p.ProductID, title: p.Title}
|
|
index[p.ProductID] = i
|
|
}
|
|
|
|
var items []model.ProductItem
|
|
if err := postgres.SELECT(table.ProductItem.AllColumns).
|
|
FROM(table.ProductItem).
|
|
QueryContext(ctx, s.db, &items); err != nil {
|
|
return nil, fmt.Errorf("payments: load catalog items: %w", err)
|
|
}
|
|
for _, it := range items {
|
|
if i, ok := index[it.ProductID]; ok {
|
|
entries[i].atoms = append(entries[i].atoms, atomQty{atomType: it.AtomType, quantity: int(it.Quantity)})
|
|
}
|
|
}
|
|
|
|
var prices []model.ProductPrice
|
|
if err := postgres.SELECT(table.ProductPrice.AllColumns).
|
|
FROM(table.ProductPrice).
|
|
QueryContext(ctx, s.db, &prices); err != nil {
|
|
return nil, fmt.Errorf("payments: load catalog prices: %w", err)
|
|
}
|
|
for _, pr := range prices {
|
|
i, ok := index[pr.ProductID]
|
|
if !ok {
|
|
continue // a price for a deactivated product — not in the storefront
|
|
}
|
|
method := ""
|
|
if pr.Method != nil {
|
|
method = *pr.Method
|
|
}
|
|
entries[i].prices = append(entries[i].prices, priceRow{method: method, currency: Currency(pr.Currency), amount: pr.Amount})
|
|
}
|
|
return entries, nil
|
|
}
|