6e03ce0131
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 22s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m57s
Accept real money via Telegram Stars (XTR) — the third intake rail alongside Robokassa (direct) and VK Votes. Only the bot reaches Telegram, so the rail funnels through the reverse mTLS bot-link: - the gateway mints the invoice on a CreateInvoice command (the bot calls createInvoiceLink, XTR; the link goes to WebApp.openInvoice); - the bot gates each pre_checkout_query via a ValidatePreCheckout unary (the order must exist, be still creditable and not already paid — the reusable-invoice double-pay guard; the decline reason is localised to the order account's language); - a completed successful_payment is queued in a durable pure-Go SQLite outbox and forwarded via a ForwardPayment unary, credited once (idempotent on telegram_payment_charge_id, honours an expired order), re-driven on restart and every 30s. The rail is wired by TELEGRAM_STARS_OUTBOX_DIR (default /data) but stays inert until a chip pack carries an XTR price, so seeding a Stars price in the admin is the go-live. Tests: backend integration (order->forward->credit once, duplicate, pre_checkout gate) + bot outbox unit (idempotent, restart re-drive) + executor createInvoice. Docs: PAYMENTS(+ru) §9, ARCHITECTURE, the platform/telegram README, PLAN.
367 lines
32 KiB
Markdown
367 lines
32 KiB
Markdown
# 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) | нет — заморожено (только просмотр) | — |
|
||
| Внутри Telegram | `telegram` | — |
|
||
| Web / native (Direct)| `direct` + `vk` + `telegram` | direct → vk → tg |
|
||
|
||
- Внутри VK/TG доступен только одноимённый сегмент; всё остальное (в первую очередь
|
||
`direct`) там невидимо как тратимое.
|
||
- В вебе у стора нет юрисдикции, поэтому доступны все привязанные сегменты, списываются по
|
||
приоритету direct → vk → tg.
|
||
- **VK iOS** заморожен для траты (Apple запрещает тратить виртуальную валюту на цифровые
|
||
товары мимо IAP внутри 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 — **невозвратно**, пользователю возврат не предлагаем. Админ может сделать
|
||
**ручной** возврат (крайний случай: пользователь требует вскоре после оплаты / закрывает
|
||
аккаунт — связано с `accountdelete`, где уже сохраняются сообщения). **Внешние** возвраты
|
||
(чарджбек / решение стора / TG / VK) обрабатываются: система принимает событие `refunded`,
|
||
**по возможности** отзывает бенефит (в минус не уходим; если Фишки уже потрачены —
|
||
фиксирует убыток + флаг защиты от злоупотреблений), пишет в журнал. Журнал операций
|
||
**спроектирован экспортопригодным** для будущей налоговой отчётности и сверки с Robokassa
|
||
(саму сверку пока не строим; схема остаётся совместимой).
|
||
|
||
## 10. Реклама
|
||
|
||
**Охват на старте: только VK** для видео (награда в рублях, ОРД автоматом, API готов).
|
||
web/native/TG держат только существующий наш **текст-баннер**; видео отложено до появления
|
||
рублёвой in-app сети. Рекламный провайдер за **абстракцией**, чтобы будущая сеть для других
|
||
платформ встроилась без переделки. Крипто-сети (AdsGram/AdMob) отвергнуты — нет легального
|
||
рублёвого дохода самозанятому (НПД).
|
||
|
||
**Ролик за награду** (добровольное видео за Фишки) начисляет Фишки через платёжный домен по
|
||
**серверному verify-колбэку** сети (клиенту не верим, как платежу). Антифрод на старте — только
|
||
verify провайдера (своего дневного потолка пока нет; абстракция позволит добавить лимиты
|
||
позже).
|
||
|
||
**Полноэкранный ролик** (после хода), конфигурируемые серверные значения:
|
||
|
||
- Глобальный кулдаун **на пользователя, сквозь все партии**, дефолт **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).
|