Files
scrabble-game/docs/PAYMENTS_ru.md
T
Ilia Denisov 13be7c3d9a
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 23s
CI / ui (pull_request) Successful in 1m12s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m53s
feat(ads): VK post-move interstitial + retire deprecated hint_balance/paid_account
Interstitial video after a confirmed play or a hint, VK-only, offline
banner-only. The gate is client-mirrored: the backend puts the config
cooldowns (global 5m / vs_ai 30m / hint 1m) and a suppressed flag (the
no-ads / no_banner gate, same as the banner) on Profile.ads via adsFor;
the client self-gates on a per-kind last-shown time in localStorage, with
the VITE_ADS_STUB contour "ad fired" toast. Never fires after a pass,
exchange or resign. maybeShowInterstitial + vkShowInterstitial; the codec
and gateway transcode carry the ads block.

Also retires the deprecated accounts.hint_balance / paid_account domain
usage (expand-contract, code only — the columns stay for a later DROP so
image rollback stays DB-safe): drop the Account fields and their scan, the
dead account.SpendHint, account.GrantHints and the admin grant-hints
action; the in-game hint display now comes wholly from the payments hint
benefit. Banner eligibility and account merge no longer read the legacy
flags.

Tests: ads.test.ts (the client-mirrored gate), the codec ads round-trip,
the gateway ads transcode test, and a profile ads-config integration test
(cooldowns + suppressed under no-ads / no_banner). Docs: PAYMENTS §10 (+ru)
interstitial, the decision amends, backend README, the plan.
2026-07-10 02:48:10 +02:00

387 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PAYMENTS — механики монетизации
Русское зеркало [`PAYMENTS.md`](PAYMENTS.md) (английская версия — основная). Описывает
домен монетизации: игровую валюту, кошельки, ценности, правила комплаенса сторов, приём
платежей, рекламу, каталог, журнал операций и отчёты. Каждую правку `PAYMENTS.md` зеркалим
сюда в том же PR (как `FUNCTIONAL.md`/`FUNCTIONAL_ru.md`).
Читать перед любым изменением платёжного поведения. Технический план внедрения —
[`../PLAN.md`](../PLAN.md).
> Статус: спецификация утверждена; внедрение поэтапно (см. `PLAN.md`). В проде пока ничего
> из этого нет.
## 1. Обзор
Игра зарабатывает двумя независимыми каналами:
- **Покупки** игровой валюты **«Фишка»** за реальные деньги, затем трата Фишек на
**ценности** (бенефиты).
- **Реклама** — ролик за награду пополняет Фишки; полноэкранный ролик и наш баннер
зарабатывают показами.
Валюта **двухуровневая**:
```
деньги (Голоса VK / Stars TG / рубли через Robokassa) ─┐
├─► Фишки ──► ценности
просмотр ролика за награду ─┘ (без рекламы,
подсказки,
турнирный взнос)
```
Фишки — единая единица витрины. Деньги и просмотры **пополняют** Фишки; Фишки **покупают**
ценности. Прямой оплаты ценности деньгами нет — всегда через Фишки.
## 2. Модель валюты
**Одна Фишка равна одной Фишке везде** — единица единая. Разница между методами оплаты
целиком в **курсе покупки**: пакет Фишек стоит *X* Голосов / *Y* Stars / *Z* рублей, и курс
учитывает комиссию каждого стора. Цены ценностей фиксированы **в Фишках** и одинаковы для
всех методов.
Баланс Фишек **сегментирован по «источнику» (`source`)** — платформе, где Фишки пополнены:
| `source` | Чем пополняется |
|------------|------------------------------------|
| `vk` | Покупка за Голоса VK, ролик за награду в VK |
| `telegram` | Покупка за Stars TG |
| `direct` | Покупка через Robokassa (web / native) |
Один аккаунт держит все три сегмента одновременно: `баланс = (account_id, source)`, до трёх
записей — запись материализуется лениво при первом пополнении сегмента, отсутствующий сегмент
читается как ноль. Сегментация **не** по привязке (identity) — см. §6.
Почему сегментировано, а не один общий баланс: правила сторов запрещают активировать
ценность, оплаченную мимо их кассы. Фишки, пополненные внутри VK (Голоса), тратятся только
в контексте VK; Stars — только внутри Telegram; Фишки от Robokassa (`direct`) — только вне
сторов. См. §4.
## 3. Три операции, которые нельзя путать
Не смешивать — у них разные ключи:
1. **Пополнить Фишки** — деньги/реклама → сегмент `source` Фишек, по **контексту
исполнения**.
2. **Потратить Фишки = купить ценность** — сегмент → бенефит, по контексту с гейтом (§4).
Здесь бенефит **рождается** и помечается своим `origin`.
3. **Применить бенефит** во времени (напр. «без рекламы до *T*») — по правилу `origin`
(§5).
`source` (где пополнены Фишки) и `origin` (где куплена ценность) используют одно множество
значений `{vk, telegram, direct}`, но значат разное. В вебе они расходятся: покупка в вебе
может списать `vk`-Фишки (`source=vk`) в `direct`-покупку (`origin=direct`).
## 4. Гейт комплаенса сторов
Стена комплаенса **односторонняя**. Опасное направление — активация оплаченной снаружи
ценности *внутри* обёртки стора — заблокировано; безопасное (бенефит стора действует
наружу, в открытом вебе) — разрешено.
**Контекст траты → какие сегменты/бенефиты доступны:**
| Контекст исполнения | Тратимые сегменты Фишек | Приоритет траты |
|----------------------|---------------------------|--------------------|
| Внутри VK (Android) | `vk` | — |
| Внутри VK (iOS) | `vk` (заморожена покупка) | — |
| Внутри Telegram | `telegram` | — |
| Web / native (Direct)| `direct` + `vk` + `telegram` | direct → vk → tg |
- Внутри VK/TG доступен только одноимённый сегмент; всё остальное (в первую очередь
`direct`) там невидимо как тратимое.
- В вебе у стора нет юрисдикции, поэтому доступны все привязанные сегменты, списываются по
приоритету direct → vk → tg.
- **VK iOS — заморозка ПОКУПОК, а не траты.** ToS Apple запрещает там только **покупать**
внутриигровые ценности (за любую валюту) — поэтому покупка (деньги → Фишки) отклоняется. А
**тратить** Фишки VK-кошелька (заработанные рекламой или купленные на том же VK-аккаунте, напр. в
VK Android) и зарабатывать их — легально и на VK iOS разрешено; купленный бенефит там тоже
*действует*. Блокируется только шаг «деньги внутрь».
Аккаунт **единый** (привязки сливаются, один профиль/друзья/статистика). Гейт
**логический**: в контексте VK/TG сервер активирует только одноимённый сегмент. Держится на
**доверенном сигнале платформы** (§8) — клиенту не верим. Когда платформу нельзя доверенно
установить, гейт **fail-closed**: траты/покупки запрещены, только просмотр.
### Одностороннее применение бенефита
Бенефит несёт `origin` = **контекст покупки** (не «чем оплачено»). Покупка в вебе за
`vk`-Фишки всё равно даёт `origin=direct`.
| `origin` | Где действует бенефит |
|------------------|----------------------------------------------------|
| `vk` / `telegram`| **Везде** — внутри своего стора *и* наружу в web/native |
| `direct` | **Только** web/native — никогда внутри VK/TG (= бан) |
Перед тратой в вебе `vk`/`telegram`-Фишек интерфейс **предупреждает**, что ценность будет
доступна только здесь (web/native) из-за ограничений VK/TG.
## 5. Ценности
Три разные сущности, не смешивать:
- **Фишки** — валюта. Сегмент по `source`.
- **Подсказки** — расходник, покупается за Фишки. Сегмент по `origin`. Тратятся по одной в
онлайн-играх; в `vs_ai` подсказки бесплатны/безлимитны (30-мин кулдаун, не в счёт).
Подсказка в игре списывается из `origin`, применимого в текущем контексте (то же
одностороннее послабление).
- **Без рекламы** — срок-бенефит, покупается за Фишки. Сегмент по `origin`.
**Складывание «без рекламы».** Покупка срока продлевает `paid_until[origin] += срок` от
`max(сейчас, текущий конец)` — остаток не теряется («сроки плюсуются»). **Навсегда**
отдельный вечный флаг, перекрывает сроки. «Реклама выключена в контексте *P*» ⇔ есть
применимый в *P* `origin` с `paid_until > сейчас` (в вебе берём максимум по direct/vk/tg; в
VK только vk; в TG только tg).
**Что гасит «без рекламы»:** верхний баннер **и** полноэкранный ролик после хода.
Добровольный **ролик за награду** (за Фишки) не гасится — это выбор пользователя.
**Турнирный взнос** — будущий тип ценности; атом заложен, механика позже.
## 6. Жизненный цикл кошелька
**Правило доступности сегмента.** Сегмент тратим ⇔ на аккаунте есть привязка этого
`source` (для `direct` — устойчивая привязка/email). Отсюда unlink/мерж выводятся
естественно.
**Отвязка (vk/tg).** Разрешена даже при ненулевом балансе/активном бенефите. Сегмент не
сжигается — он **засыпает** (нет привязки ⇒ недоступен в VK *и*, без привязки, недоступен
как подтянутый веб-сегмент); повторная привязка будит. Перед отвязкой предупреждение
(«N Фишек станут недоступны до повторной привязки»). Последнюю привязку отвязать нельзя
(существующий `ErrLastIdentity`).
**Мерж.** Сегменты и бенефиты сливаются **по origin**: одноимённые складываются (Фишки
суммируются, сроки бенефита продлеваются по origin), разные сосуществуют. Это расширяет
текущий мерж аккаунтов (`hint_balance +=`, `paid_account OR=`), так что origin сохраняется и
ничего не протекает между платформами.
**Гость.** У гостевого аккаунта **вообще нет кошелька** — раздел «Кошелёк» скрыт, покупок
нет, баланса нет, by design. Балансы — только у durable-аккаунтов. Поэтому чистильщик
гостей удаляет их свободно (денег там быть не может). В `direct` email обязателен **перед
первой покупкой** как якорь восстановления (в VK/TG якорь — сама vk/tg-привязка);
переиспользуем существующий флоу email (запрос кода → подтверждение → снятие флага гостя →
durable).
## 7. Каталог и цены
Каталог **конфигурируемый** — продукты, цены, курсы покупки и награда за ролик живут в базе
и правятся в админке, без релиза.
- **Базовые ценности (атомы):** Фишки, подсказки, дни без рекламы, участие в турнире.
- **Продукт = набор атомов + цена.** Продаётся по одной или комбо (напр. «250 подсказок +
30 дней без рекламы»).
- **Пакет Фишек** (пополняет Фишки) — цена **per-метод** (мультивалютная Голоса/Stars/руб)
одним продуктом.
- **Ценность** (за Фишки) — цена **в Фишках** (единая для всех методов).
**Деактивация, не удаление.** Продукты деактивируются (soft-delete). Состоявшаяся покупка
хранит **снимок** проданного (состав атомов + цена на момент) в архиве, чтобы
история/чеки/налоги не зависели от последующих правок каталога.
## 8. Доверенный сигнал платформы
Гейту (§4) нужен **доверенный, неподделываемый** контекст платформы на сервере. Клиент —
никогда не источник правды.
- Платформа — **свойство сессии**, фиксируется при создании сессии. Обёртки VK и Telegram
пересоздают сессию (и потому заново проверяют подпись запуска — VK launch-params `sign` /
Telegram `initData`) на **каждом холодном старте**, поэтому их платформа переподтверждается
при каждом запуске; `direct`-сессия фиксирует платформу один раз, самим фактом создания
веб/native-сессии (внешней подписи нет и не нужно — доступ к vk/tg-сегментам в direct-контексте
всё равно требует реальной привязки, §6).
- Платформа несёт **kind** (`vk`/`telegram`/`direct`) **плюс подтип** (`ios`/`android`/`web`).
`kind` доверенный всегда — сервер выводит его из проверенного запуска, не из клиентского поля.
**Подтип доверенный только у VK**: он лежит внутри подписанных параметров запуска, что и делает
**заморозку VK iOS** выполнимой; у Telegram и direct подтип сообщает клиент, он best-effort, и
гейт на него не опирается.
- Гейтвей резолвит сессию и передаёт `platform` в бэкенд (рядом с существующим `X-User-ID`),
беря его из сессии — не из тела клиентского запроса.
- **Fail-closed:** недоверенная платформа — сессия без записанной платформы, созданная до этой
функции или которую гейтвей не смог атрибутировать — запрещает траты/покупки и применение
любого чужого origin (только просмотр). VK/TG-сессия восстанавливается на следующем холодном
старте (пересоздание), переиспользуемая direct/email-сессия — при повторном входе.
## 9. Приём платежей
**Только серверный колбэк провайдера.** Фишки начисляются лишь по **проверенному**
(подпись/HMAC) серверному колбэку — Robokassa webhook / TG `successful_payment` / VK
callback. Клиентское «я оплатил» игнорируется.
**Единственный писатель.** Один платёжный домен — единственный, кто пишет в журнал операций.
Публичные вебхуки (Robokassa/VK) терминируются на краю (Caddy/gateway) и проксируются в
платёжный домен; TG `successful_payment` приходит боту, тот форвардит в платёжный домен.
Одно место начисляет и защищает от повторов.
**Флоу заказа.** Сервер заранее создаёт `order(pending)` с account / платформой / пакетом /
ожидаемой суммой / origin. `order_id` прокидывается провайдеру (Robokassa `InvId` / TG
`invoice_payload` / VK `item` — точную форму VK уточнить при интеграции). Колбэк матчится по
`order_id` (никогда по сумме, поэтому коллизии одинаковых сумм невозможны), сверяет сумму,
начисляет, помечает `paid`. **Защита от повторов:** дедуп по `(провайдер,
provider_payment_id)`.
**Pending невидим** пользователю; авто-истекает по таймауту (~30 мин, гигиена базы).
Валидный колбэк исполняется **всегда**, даже на истёкшем заказе (`expired` ≠ отмена —
деньги реальны, Фишки должны быть выданы). Пользователь видит только успешные покупки.
**TG Stars.** До Telegram дотягивается только **бот**, поэтому весь рельс идёт через обратный
mTLS **bot-link** (бот ↔ gateway; напрямую к бэкенду бот не ходит). Инвойс создаёт бот: на пути
заказа gateway шлёт команду `CreateInvoice`, а бот возвращает `createInvoiceLink` (XTR) в Ack,
который Mini App открывает через `WebApp.openInvoice`. До списания звёзд бот отвечает на
`pre_checkout_query` через унарный вызов бот→gateway `ValidatePreCheckout` (за ним — приём): одобрить,
только если заказ существует, ещё оплачиваем и **не оплачен ранее** — ссылка Stars-инвойса
переиспользуема, так что этот гейт — единственное место, где повторная оплата отсекается **до**
списания; текст отказа локализован в язык аккаунта заказа.
**Outbox TG-бота.** `successful_payment` приходит только боту (Bot API, не Mini App), а хост бота
слабый и может терять связь, поэтому бот — durable-звено. Store-and-forward на **SQLite** на диске
бота (`internal/outbox`): сохранил при получении (идемпотентно по `telegram_payment_charge_id`) →
форвардит по bot-link (унарный `ForwardPayment`; gateway проксирует в приём) → при durable-ответе
пометил `forwarded`. Дореталивает недоставленное при рестарте и по периодическому тику. Доставка
at-least-once + идемпотентный приём (дедуп по `telegram_payment_charge_id`) = начисление ровно один
раз.
**События.** Платёжный домен пишет `payment_events` (succeeded / failed / refunded);
диспетчер рассылает по каналам — live gRPC-стрим, если пользователь в аппе, иначе
существующий пуш `botlink` / email. «Оплата не прошла» (**активный** отказ провайдера, не
брошенный pending) доводится до пользователя; «оплата прошла» — хук (письмо / сообщение в
бота).
**Возвраты.** ToS — **невозвратно**, пользователю возврат не предлагаем. Возвраты **инициирует
админ** (консоль E7): ни один рельс не шлёт непрошеный возврат — Robokassa через refund-API / ЛК
(авто-опрос статуса — отложенный воркер, при низком объёме чарджбеков не оправдан), VK — через
поддержку, TG Stars — вызовом `refundStarPayment`. Все сходятся на одном движке — метод `Refund`
(`internal/payments`): матчит оплаченный заказ, пишет **refund**-строку журнала (идемпотентно по
`(provider, provider_refund_id)` — refund-id отличается от payment-id fund'а, поэтому строки
сосуществуют под тем же partial-unique индексом) и **по возможности отзывает начисленные Фишки с
полом 0** (в минус не уходим — D27, `balances_chips_chk`). Если Фишки уже потрачены, невозвратный
остаток фиксируется как **убыток + флаг злоупотребления** per-account (`payments.account_risk`, читает
отчёт E7). Дельта Фишек в refund-строке — то, что реально отозвано, поэтому журнал остаётся сверяемым
с балансом; **полный** реверс (деньги, исходные Фишки, убыток) лежит в snapshot строки. Заказ остаётся
`paid` — возврат живёт в журнале + событии `refunded`, не в статусе заказа. Повторный возврат не
отзывает ничего. Журнал операций **спроектирован экспортопригодным** для будущей налоговой отчётности
и сверки (саму сверку пока не строим; схема остаётся совместимой).
## 10. Реклама
**Охват на старте: только VK** для видео (награда в рублях, ОРД автоматом, API готов).
web/native/TG держат только существующий наш **текст-баннер**; видео отложено до появления
рублёвой in-app сети. Рекламный провайдер за **абстракцией**, чтобы будущая сеть для других
платформ встроилась без переделки. Крипто-сети (AdsGram/AdMob) отвергнуты — нет легального
рублёвого дохода самозанятому (НПД).
**Ролик за награду** (добровольное видео за Фишки) начисляет Фишки через платёжный домен. **VK Mini
App отдаёт только клиентский результат просмотра** (`VKWebAppShowNativeAds``data.result`) —
серверной проверки нет — поэтому начисление **client-attested** (D29 амендим: server-verify у VK
невозможен). Защита — **серверный дневной и часовой кап** (config `reward_daily_cap` /
`reward_hourly_cap`, дефолт 50 / 10): он ограничивает читера, который пропускает ролик и дёргает
эндпоинт напрямую, и — не менее важно — лимитирует бесплатные Фишки, чтобы желающий больше **покупал**.
Начисление идемпотентно по клиентскому nonce и order-less; выплата — config
(`rewarded_payout_chips`, дефолт 0 = ролик выключен, пока не задан). Rewarded только в VK и не
гасится «без рекламы» (D9). Сеть с серверным verify встроится за ads-абстракцией. На тест-контуре
build-флаг (`VITE_ADS_STUB`) подменяет ролик тостом; прод всегда крутит настоящую рекламу.
**Полноэкранный ролик** (после подтверждённого хода), **только VK**, конфигурируемые серверные
значения. Гейт **зеркалится на клиенте**: профиль несёт кулдауны и флаг `suppressed` (тот же гейт
«без рекламы» / `no_banner`, что и у баннера, считается на сервере в `adsFor`), а клиент сам
гейтит по времени последнего показа **на каждый вид** в `localStorage` — без серверного раунд-трипа
на каждый ход. Контурный `VITE_ADS_STUB` подменяет ролик тем же тостом «ad fired». Значения:
- Глобальный кулдаун **на пользователя, сквозь все партии**, дефолт **5 мин**.
- **`vs_ai` — 30 мин** (соосно кулдауну подсказок, чтобы не отпугивать казуалов).
- Применение **подсказки** триггерит ролик **независимо** от основного кулдауна, со своим
кулдауном **1 мин**.
- Показывается **только после подтверждённого хода или подсказки** — никогда после пропуска,
обмена или сдачи (ролик за не-очковое действие только раздражает, за них не «награждаем»).
- Оффлайн — только баннер.
- Уважать собственные лимиты частоты VK.
**Оффлайн без рекламы**, кроме нашего баннера. Бенефит `без рекламы` гасит баннер через
существующий `ads.Eligible` (`backend/internal/ads/ads.go`), который расширяется до гейта по
**origin-бенефиту, применимому в текущем контексте**, а не по одному глобальному флагу.
## 11. Админ, аудит, отчётность
**Неизменяемый журнал операций + материализованный баланс.** Журнал операций **только на
INSERT** (никогда UPDATE/DELETE — полный аудит). Балансы сегментов `(account, source)` и
бенефиты `(account, origin)` — быстрый **материализованный** кэш, обновляется **в той же
транзакции**, что и запись журнала, и пересчитывается из журнала для сверки.
**In-process кэш чтения.** Поверх материализованных таблиц пакет payments держит
in-process кэш сегментов и бенефитов по ключу-аккаунту (write-through), чтобы горячие пути
чтения — проверка показа рекламы, доступность подсказок, экран кошелька, гейт траты — на
устоявшемся пути **не** делали ни одного запроса к схеме `payments`. Кэш инвалидируется на
каждой изменяющей операции payments (трата / грант / пополнение / возврат / мерж) и
перечитывается из материализованных таблиц при промахе (тот же write-through-паттерн, что и у
гейта блокировок аккаунта). Один инстанс — под текущий деплой; многоинстансный backend
потребовал бы общего кэша. Присутствие identity (какие сегменты «не спят», §6) передаёт
вызывающий, здесь не кэшируется, поэтому отвязка/повторная привязка действует сразу.
**Награждение админом.** Админ начисляет **только конкретные ценности** (без рекламы /
подсказки) — **никогда не Фишки** (подаренный баланс валюты = обход кассы стора). Админ
**выбирает origin** при выдаче (ответственность за комплаенс на нём: `origin=vk`
точечно/малый объём = низкий риск, `origin=direct` = безопасно). Грант — транзакция журнала
типа `admin_grant`, цена 0 Фишек — полный аудит наград.
**Финансовый отчёт по пользователю** в админке `/_gm` — балансы сегментов, платежи, траты,
гранты, возвраты, полная история — расширение существующей карточки (`UserDetailView`,
`handlers_admin_console.go`). Плюс экспорт журнала.
## 12. Налоги и комплаенс
Чеки формируются автоматически **на стороне провайдера** и отличаются по каналу:
- **Robokassa** (direct) — чек НПД самозанятого при оплате.
- **VK** — VK сам процессит Голоса через налоговую; делать нечего.
- **TG Stars** — налоговой стороны нет (для РФ-самозанятого Stars легально невыводимы = не
доход НПД; принимаем, чек не формируем).
**ОРД** (маркировка рекламы) по VK-рекламе — на стороне VK. (Не юридическая консультация —
владелец сверяет точную схему НПД с налоговым консультантом.)
## 13. Дистрибуция (native Android)
- **RuStore** — Robokassa/внешний гейт разрешён (0%); native = чистый контекст `direct`.
- **Google Play** — direct-покупки **скрыты**; «Кошелёк» показывает заглушку («установите
версию из RuStore для покупок»). Ролик за награду и трата уже накопленных Фишек работают.
Перед GP-релизом свериться с актуальными правилами Google по внутренней валюте.
## 14. Модель данных (схема `payments`)
Платёжный домен живёт в **своей схеме `payments`** в общем инстансе Postgres, со своим
DB-ролём (права только на `payments`) и доменным пакетом за жёстким интерфейсом.
**Cross-schema внешнего ключа** к `backend.accounts` **нет** — идентификатор аккаунта здесь
обычное значение, согласуемое в коде и связываемое с tombstone-аккаунтом / досье
retained-identities по стабильному id. Это держит трату «Фишки↔бенефит» атомарной **внутри
`payments`** и делает домен извлекаемым в свою базу. Сохранность — **PITR** (непрерывный
WAL-архив), независимо от топологии базы.
Основные таблицы (финальные имена/колонки — в `PLAN.md`):
- **журнал операций (ledger)** — append-only операции: пополнение / трата / `admin_grant` /
возврат; `(провайдер, provider_payment_id)` уникален для защиты от повторов;
экспортопригоден.
- **балансы** — материализованные `(account_id, source) → Фишки`.
- **бенефиты** — материализованные `(account_id, origin)` → «без рекламы»
`paid_until`/`forever` + счётчик подсказок.
- **каталог** — атомы + продукты (деактивируемые), цены per-метод, курсы покупки Фишек,
награда за ролик.
- **заказы (orders)** — pending-покупки, `order_id`, ожидаемая сумма, origin, статус
(pending/paid/expired).
- **payment_events** — succeeded/failed/refunded для диспетчера.
Legacy `accounts.hint_balance` и `accounts.paid_account` **устаревают** в пользу
сегментированной модели и удаляются в отдельной **contract-миграции** (expand-contract, после
того как валютное ядро переключит чтения, и после Release 2 — откат образа остаётся безопасным
для БД); ни то, ни другое в проде никогда не выставлялось (потока покупки не было), поэтому
legacy-значения обнуляются.
## 15. Словарь
- **Фишка** — игровая валюта; единая единица, сегментирована по `source`.
- **source (источник)** — где пополнены Фишки (`vk`/`telegram`/`direct`).
- **origin (происхождение)** — где куплена ценность; определяет, где действует бенефит.
- **ценность / бенефит** — то, что покупается за Фишки (без рекламы, подсказки, турнирный
взнос).
- **гейт** — одностороннее правило комплаенса сторов (§4).
- **журнал операций (ledger)** — неизменяемая запись всех операций с деньгами/ценностями.
- **платёжный канал / рельса** — платёжный провайдер (Robokassa / Голоса VK / Stars TG).