Let an operator disable purchases live from the admin — a whole rail/channel or
one account — and show the user a localized reason on their next attempt, so a
provider outage or a misconfig is explained instead of a silent dead button.
- rail kill switch (payments.rail_status, per rail direct:web / direct:android /
vk / telegram): enabled + a per-language message, edited on the catalog page.
Fail-open — a rail with no row stays enabled, so payments are never accidentally
killed. The intake gate (CanPurchase in handleWalletOrder, before the order)
returns payment_unavailable + the localized message, orthogonal to the security
gates.
- per-account override (payments.account_payment_override, a row only for
non-default): allow / deny / default, edited on the user card. "allow" bypasses
ONLY the ops rail switch, never the security gates (trusted platform, the email
anchor, the VK-iOS freeze, the min client version).
- wire: an additive ExecuteResponse.message envelope field (frozen-contract-safe);
the gateway forwards a backend domain-error message; the client shows it on a
payment_unavailable buy attempt.
- admin: rail toggles on the catalog page, the override control on the user card.
- tests: the pure gate (unit, TDD), the store + gate + override end-to-end
(integration, migration 00016), the client (svelte-check / vitest).
- docs: PAYMENTS (+ru), the decisions log (D45/D46). Fiscalization stays
cabinet-side (owner decision) — no itemized-receipt code.
Contour-safe: additive migration (two new tables, no wipe), the wire add is
additive, and fail-open so nothing is disabled until an operator acts.
The "watch for chips" rewarded payout and its per-day / per-hour caps live in the
shared payments.config row and had no admin surface — they could only be changed by
SQL, contrary to D32 (the rewarded rate is meant to be admin-configurable). Add a
"Rewarded ads" form on the /_gm/catalog page: RewardConfig / SetRewardConfig on the
service (non-negative validated), a setRewardConfig store writer (the singleton
config row), the consoleSetReward handler + POST /_gm/catalog/reward route, and the
form pre-filled from the current config. No migration — the columns already exist.
The reward rate is a config value, not a sellable catalog atom (so a "no-ads forever"
style product is still out of scope by D32/D33).
Test: an integration test sets the config, checks the page pre-fill, and refuses a
negative value.
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.
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.
The /_gm user card gains a Grant panel: grant raw benefit atoms (hints /
no-ads days / forever) or a defined value product (a reward bundle, including
an archived one), origin-picked. Both write an admin_grant ledger row via
payments.Grant / GrantProduct; the by-product grant records the source
product_id + snapshot. Both refuse a chips atom (never grant currency) or a
tournament atom (no credit target yet); chips/tournament products are also
kept out of the by-product picker.
Tests: the console grant end to end (raw, by-product, refuse a chips pack,
CSRF-guarded).
The /_gm console gains a Catalog editor — the source of truth for products.
Create / edit / archive-unarchive products, their atom composition and per-rail
prices (RUB via direct / VOTE via vk / XTR via telegram / CHIP value), and
hard-delete only a never-transacted product (an order or ledger reference forces
archive-only, backed by the FK RESTRICT). The archived flag reuses the existing
product.active. Activation revalidates the sellable shape — a pack (the chips
atom ⇒ a money price per rail, chips-only) or a value (no chips ⇒ a CHIP price);
a tournament-bearing product is composable but never sellable yet.
Backed by payments AdminCatalog / CreateProduct / UpdateProduct /
SetProductActive / DeleteProduct + a pure validateProduct.
Tests: validateProduct (pack / value / tournament / duplicate / shape); the
console editor end to end (create, edit, archive, delete-if-clean, refuse a
transacted delete).