feat(payments): chip wallet, store-compliance gate and benefit application
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Successful in 1m7s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Successful in 1m7s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
Stand up the internal chip/benefit mechanic behind the narrow payments interface: context-aware balances and benefits, an atomic chip spend, admin grants as zero-price value sales, the one-directional store-compliance gate (VK/TG same- origin only, web draws direct→vk→tg, VK-iOS frozen, untrusted fail-closed), and per-origin hint and no-ads application with term stacking. Reads are served from an in-process, account-keyed write-through cache (mirroring the suspension gate), so hot paths issue no query to the payments schema. Flip the online-game hint wallet and the ad-banner suppression from the deprecated accounts.hint_balance / paid_account columns to the payments benefit (a hint balance no longer suppresses the banner — only a no-ads benefit does), and fold chip segments and benefits by origin on account merge, inside the merge tx. Add the GET/POST /api/v1/user/wallet edge chain (REST → Connect → FlatBuffers) plus its codec unit test; no wallet UI yet. Bring the frozen owner decisions log into the repo at docs/PAYMENTS_DECISIONS_ru.md (it was untracked under .vscode) and reference it from PLAN.md; record the read-cache design and the present-sources interface in PLAN.md and docs/PAYMENTS.md (+ RU mirror).
This commit is contained in:
@@ -273,6 +273,16 @@ the **origin benefit applicable in the current context** rather than one global
|
||||
`(account, origin)` are a fast **materialized** cache, updated **in the same transaction** as
|
||||
the ledger write, and recomputable from the ledger for reconciliation.
|
||||
|
||||
**In-process read cache.** On top of the materialized tables, the payments package keeps an
|
||||
in-process, account-keyed, write-through cache of each account's segments and benefits, so the
|
||||
hot read paths — the ad-eligibility check, hint availability, the wallet view, the spend gate
|
||||
— issue **no** query to the `payments` schema on the steady-state path. It is invalidated on
|
||||
every payments mutation (spend / grant / fund / refund / merge) and re-read from the
|
||||
materialized tables on a miss (the same write-through pattern the account-suspension gate
|
||||
uses). Single-instance, matching the deployment; a multi-instance backend would need a shared
|
||||
cache. Identity-presence (which segments are awake, §6) is supplied by the caller, not cached
|
||||
here, so unlink/re-link takes effect immediately.
|
||||
|
||||
**Admin rewards.** An admin grants **concrete values only** (no-ads / hints) — **never
|
||||
chips** (a gifted currency balance = a store cash-desk bypass). The admin **picks the
|
||||
origin** at grant time (compliance is on them: `origin=vk` point-wise/low-volume = low risk,
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
# Монетизация scrabble-game — проектирование и внедрение
|
||||
|
||||
## Context
|
||||
|
||||
Игра в проде (`erudit-game.ru`), мультиплатформенная (VK / Telegram Mini App /
|
||||
web+PWA / native Android+iOS через Capacitor). Владелец (самозанятый, НПД) хочет
|
||||
ввести монетизацию: игровая валюта «Фишка», раздельные кошельки по платформам,
|
||||
покупка бенефитов (без рекламы, подсказки, будущий турнирный взнос), rewarded-реклама
|
||||
как канал пополнения, строгий транзакционный лог и админ-отчёты.
|
||||
|
||||
Отправная точка — `monetization-prerequisites.md` (пожелания владельца). Цель этого
|
||||
процесса: (1) через интервью выстроить чёткую модель, (2) оформить и поддерживать
|
||||
`PAYMENTS.md` (механики + договорённости), (3) составить `PLAN.md` (поэтапное
|
||||
внедрение от основ к интеграциям).
|
||||
|
||||
### Что уже есть в коде (переиспользуем)
|
||||
|
||||
- `accounts.hint_balance int` (`CHECK >= 0`) + атомарный `SpendHint` / `GrantHints`
|
||||
(`backend/internal/account/account.go:551-595`) — существующий «кошелёк подсказок»,
|
||||
но пополняется только админом, покупки нет.
|
||||
- `accounts.paid_account bool` — пожизненный флаг «без рекламы», уже читается в
|
||||
`ads.Eligible(...)` (`backend/internal/ads/ads.go:107`); «no purchase flow yet».
|
||||
- Реклама сейчас = внутренний server-driven **текст-баннер** (`ads` домен + UI
|
||||
`AdBanner.svelte`), гасится `paid_account` / `hint_balance>0` / роль `no_banner`.
|
||||
Rewarded-видео нет нигде.
|
||||
- Подсказки — фича готова end-to-end (кнопка, 30-мин gate, vs_ai безлимит).
|
||||
- Settings — таб-хаб (`ui/src/screens/SettingsHub.svelte`), вставка «Кошелёк» между
|
||||
«Друзья» и «Инфо» чистая.
|
||||
- Админка `/_gm` — per-user карточка уже показывает `PaidAccount`+`HintBalance`;
|
||||
прецедент денежного действия — `POST /_gm/users/:id/grant-hints`.
|
||||
- Один аккаунт держит VK+TG+email identity **разом** (kind∈telegram/vk/email/robot);
|
||||
мерж сейчас **складывает** `hint_balance` и OR-ит `paid_account`.
|
||||
- Прецедент изолированного сервиса: connector (gRPC), renderer (HTTP-sidecar),
|
||||
platform/telegram. Прецедент public HMAC-подписанного роута (экспорт партии) —
|
||||
образец для приёмника вебхуков.
|
||||
|
||||
### Greenfield
|
||||
|
||||
Валюта «Фишка», реальные рельсы (Stars / Голоса / Robokassa), rewarded-видео,
|
||||
раздельные кошельки, транзакционный леджер, PITR/репликация, серверное знание
|
||||
платформы.
|
||||
|
||||
## Зафиксированные решения (Decisions Log)
|
||||
|
||||
- **D1. Модель валюты — двухуровневая.** Деньги (Голоса/Stars/руб) и rewarded-реклама
|
||||
пополняют баланс «Фишек»; за Фишки покупаются ценности (без рекламы, подсказки,
|
||||
турнир). Витрина ценностей — в Фишках.
|
||||
- **D2. Фишка единая (1 = 1 везде).** Разница по методам оплаты сидит **в курсе
|
||||
покупки Фишек** (пакет стоит X Голосов / Y Stars / Z руб, курс учитывает комиссии
|
||||
сторов). Цена ценностей — фиксирована в Фишках, одинакова для всех методов.
|
||||
- **D3. Изоляция — схема `payments` в общем инстансе Postgres** + доменная граница
|
||||
(свой store/service/интерфейс) + отдельный DB-роль (права только на `payments`).
|
||||
Атомарность «Фишки↔бенефит» с `backend.accounts` сохраняется (cross-schema tx в
|
||||
одном инстансе). Отдельный процесс/БД отвергнуты: ломают атомарность выдачи бенефита
|
||||
(бенефиты живут на `accounts`), цена распределённых транзакций не окупается. Вынос в
|
||||
отдельный сервис оставлен как возможность на потом (граница уже чистая).
|
||||
- **D4. Сохранность — PITR** (непрерывный WAL-архив, pgBackRest/WAL-G на 2-й хост или
|
||||
объектное хранилище). Тёплая реплика — опция на потом. Durability решается этим, а не
|
||||
топологией БД.
|
||||
- **D5. Сегмент кошелька — по source (vk / telegram / direct) на аккаунте.** Баланс =
|
||||
(account_id, source), ровно 3 сегмента. Не per-identity.
|
||||
- **D6. Комплаенс-гейт — логический, аккаунт единый.** email/direct физически привязан
|
||||
(мерж, единый профиль/друзья/статистика), но в VK/TG-контексте сервер активирует
|
||||
только одноимённый сегмент; чужое (в первую очередь direct) внутри VK/TG невидимо и
|
||||
не тратится. Гейт держится на **доверенном сигнале платформы** (проставляет гейтвей
|
||||
из подписанного контекста, не тело запроса — клиенту не верим).
|
||||
- **D7. Три операции разведены:** (1) пополнение Фишек по контексту; (2) трата Фишек =
|
||||
покупка бенефита, по контексту с гейтом (в VK только vk; в вебе direct+vk+tg по
|
||||
приоритету direct→vk→tg); (3) применение бенефита во времени.
|
||||
- **D8. origin бенефита = контекст ПОКУПКИ** (не «чем оплачено»). Правило послабления:
|
||||
origin∈{vk,tg} → действует **везде** (в сторе и наружу в web/native);
|
||||
origin=direct → **только** web/native (внутрь VK/TG нельзя = бан). Срок «без рекламы»
|
||||
хранится **per-origin** (`paid_until` на origin, покупки одного origin складываются/
|
||||
сдвигают дату); «реклама выключена в контексте P» = есть применимый в P origin с
|
||||
`paid_until > now` (в вебе берём максимум direct/vk/tg, в VK — только vk, в TG — tg).
|
||||
- **D9. «Без рекламы» гасит** верхний баннер + fullscreen-ролик после хода. Добровольный
|
||||
**rewarded** (за Фишки) не трогаем.
|
||||
- **D10. UX-требование:** при трате tg/vk-Фишек **в вебе** — предупредить юзера ДО
|
||||
покупки, что купленная механика будет доступна только здесь (web/native), т.к.
|
||||
ограничения VK/TG.
|
||||
- **D11. Три сущности разведены:** Фишки (валюта, сегмент по **source** = где
|
||||
пополнено), Подсказки (расходник, сегмент по **origin** = где куплено), «Без рекламы»
|
||||
(срок, сегмент по **origin**). source и origin — одна ось значений {vk,tg,direct},
|
||||
но разная семантика; в вебе source и origin расходятся (vk-Фишки → direct-покупка).
|
||||
- **D12. Подсказки — по origin с обратным послаблением** (vk/tg→везде, direct→только
|
||||
web/native), как «без рекламы». Списание в онлайн-игре = из origin, применимого в
|
||||
контексте; переписать `SpendHint` (`game/service.go:1147`) на контекст-аварное
|
||||
списание. vs_ai подсказки остаются бесплатными/безлимитными (не в счёт).
|
||||
- **D13. `accounts.hint_balance` выводим** expand-contract (сначала игнор, потом DROP);
|
||||
legacy-баланс **обнуляем** (в проде даров не было).
|
||||
- **D14. Unlink vk/tg — разрешён, сегмент усыпляется.** Сегмент доступен ⟺ на аккаунте
|
||||
есть identity этого source (direct ⟺ есть устойчивый identity/email). Отвязка не
|
||||
сжигает баланс/бенефит — усыпляет; re-link будит. Перед отвязкой — предупреждение
|
||||
«N Фишек станут недоступны до повторной привязки». Последнюю identity отвязать нельзя
|
||||
(существующий `ErrLastIdentity`).
|
||||
- **D15. Мерж — слияние по origin:** одноимённые сегменты складываются (Фишки sum,
|
||||
сроки бенефита продлеваются per-origin), разные сосуществуют. Прямое расширение
|
||||
текущего `hint_balance +=` / `paid_account OR=`; origin сохраняется, ничего не
|
||||
протекает между платформами.
|
||||
- **D16. Админ-награждение:** админ начисляет **только конкретные ценности** (без
|
||||
рекламы / подсказки), **никогда не Фишки** (подаренный баланс валюты = обход кассы
|
||||
стора). Админ **выбирает origin** при выдаче (ответственность за комплаенс на нём:
|
||||
origin=vk точечно/малый объём = низкий риск, origin=direct = безопасно). Грант =
|
||||
транзакция типа `admin_grant` в едином леджере, цена 0 Фишек — полный аудит наград.
|
||||
- **D17. Доверенная платформа = свойство сессии,** переподтверждается свежей подписью
|
||||
(VK sign / TG `initData`) при **каждом холодном старте** (не однократно при входе —
|
||||
обёртка всё равно шлёт подпись каждое открытие). `direct` фиксируется фактом создания
|
||||
веб/native-сессии (внешней подписи нет и не нужно — доступ к vk/tg-сегментам в direct
|
||||
всё равно требует реальной привязки, D14). Backend получает `platform` при резолве
|
||||
сессии. Платформа несёт **kind** (vk/tg/direct) **+ подтип** (ios/android/web) —
|
||||
подтип обязателен (VK iOS заморожен). TG `initData`-валидатор уже есть
|
||||
(`platform/telegram/internal/initdata`).
|
||||
- **D18. Fail-closed:** недоверенная/неподтверждённая платформа (VK/TG-сессия без
|
||||
валидной подписи на старте; старая сессия без записанной платформы) → запрет
|
||||
трат/покупок/применения чужого origin, только просмотр.
|
||||
- **D19. Дистрибуция native Android:** RuStore (Robokassa разрешён, чистый direct) +
|
||||
Google Play (**direct-покупки скрыты** — на «Кошельке» заглушка «установите версию из
|
||||
RuStore для покупок»; rewarded-реклама и трата уже накопленных Фишек работают). Перед
|
||||
GP-релизом свериться с актуальными правилами Google по внутренней валюте.
|
||||
- **D20. Приём оплаты — только серверный колбэк провайдера.** Фишки начисляются лишь по
|
||||
проверенному (подпись/HMAC) серверному уведомлению: Robokassa webhook / TG
|
||||
`successful_payment` / VK callback. Клиентское «я оплатил» игнорируется.
|
||||
- **D21. Единый payments-домен — единственный писатель леджера.** Публичные вебхуки
|
||||
(Robokassa/VK) терминируются на edge (Caddy/gateway) и проксируются в payments;
|
||||
TG `successful_payment` → бот → payments. Один источник начисления/идемпотентности.
|
||||
- **D22. Order-flow с предзаказом.** Сервер создаёт `order(pending)` с account/
|
||||
платформой/пакетом/ожидаемой суммой/origin; `order_id` прокидывается провайдеру
|
||||
(Robokassa `InvId` / TG `invoice_payload` / VK `item` — точную форму VK уточнить при
|
||||
интеграции). Колбэк матчится по `order_id` (не по сумме → коллизия сумм невозможна),
|
||||
сверяет сумму, начисляет, помечает `paid`. **Идемпотентность** — дедуп по (провайдер,
|
||||
`provider_payment_id`).
|
||||
- **D23. Pending невидим юзеру;** авто-expiry по таймауту (~30 мин — гигиена БД). Но
|
||||
**валидный колбэк исполняется ВСЕГДА**, даже на истёкшем order (`expired` ≠ отмена —
|
||||
деньги реальны, обязаны выдать). Юзер видит только успешные покупки.
|
||||
- **D24. Экран «Кошелёк» — минимальный:** балансы Фишек (доступные в контексте) +
|
||||
активные бенефиты (без рекламы до даты, счётчик подсказок). **Без ленты истории**
|
||||
(шумит, отвлекает от игры). Полная история — только в админке.
|
||||
- **D25. TG-бот outbox — SQLite на диске бота.** Store-and-forward: получил
|
||||
`successful_payment` → сохранил в SQLite → подтвердил апдейт Telegram → форвардит в
|
||||
payments (идемпотентно, дедуп по `telegram_payment_charge_id`) → ack → `forwarded`.
|
||||
Ретраи с backoff, дореталивание при рестарте. At-least-once доставка + идемпотентный
|
||||
приём = начисление ровно один раз.
|
||||
- **D26. Событийный слой `payment_events`** (succeeded/failed/refunded) + диспетчер по
|
||||
каналам: live gRPC-стрим (если юзер в аппе), иначе `botlink`/email-relay
|
||||
(существующие). «Оплата не прошла» = **активный отказ провайдера** (не брошенный
|
||||
pending) — доводится до юзера. «Оплата прошла» — хук (письмо/сообщение в бота).
|
||||
- **D27. Возвраты.** ToS «невозвратно» — юзеру возврат не предлагаем. **Админ может
|
||||
сделать ручной возврат** (исключение: юзер требует вскоре после оплаты / закрывает
|
||||
аккаунт — связать с `accountdelete`, где уже правило «сообщения не трогаем»).
|
||||
**Внешние возвраты** (чарджбек / решение стора / TG / VK) — система принимает событие
|
||||
`refunded`, **по возможности** отзывает бенефит (в минус НЕ уходим; если Фишки
|
||||
потрачены — фиксируем убыток + флаг защиты от злоупотреблений), пишет в журнал.
|
||||
Журнал операций **проектируем экспортопригодным** (будущая налоговая отчётность +
|
||||
авто-сверка с Robokassa) — реализацию сверки пока не делаем, схему закладываем
|
||||
совместимой.
|
||||
- **D28. Стартовый охват видео-рекламы — только VK** (рублёвый доход, ОРД автоматом,
|
||||
API готов). web/native/TG — существующий текст-баннер. Рекламный провайдер
|
||||
закладываем **абстракцией** (будущая крутилка для других платформ встроится без
|
||||
переделки). Крипто-провайдеры (AdsGram/AdMob) отвергнуты — нет легального рублёвого
|
||||
дохода самозанятому (санкции + НПД не учитывает крипту).
|
||||
- **D29. Rewarded (добровольный ролик за Фишки)** начисляет Фишки через payments по
|
||||
**серверному verify-колбэку** рекламной сети (клиенту не верим, как платёж). Не
|
||||
гасится «без рекламы». Сколько Фишек за просмотр — в блоке экономики.
|
||||
- **D30. Частота навязанного interstitial** (конфигурируемые серверные значения):
|
||||
глобальный кулдаун **на юзера сквозь все партии**, дефолт **5 мин**; **vs_ai — 30 мин**
|
||||
(соосно кулдауну подсказок). Применение **подсказки** триггерит ролик после хода
|
||||
**независимо** от основного кулдауна, со своим кулдауном **1 мин**. Оффлайн — только
|
||||
баннер. Уважать собственные лимиты частоты VK.
|
||||
- **D31. `paid_account` тоже deprecated** → удаление из схемы (как `hint_balance`), в
|
||||
пользу per-origin бенефитов «без рекламы». Существующий `ads.Eligible`
|
||||
(`backend/internal/ads/ads.go:107`) расширяется: баннер гасится по origin-бенефиту,
|
||||
**применимому в текущем контексте**, а не по одному глобальному флагу. Legacy
|
||||
`paid_account`/`hint_balance` в проде никем не выставлены (потока покупки не было) →
|
||||
обнуляем/игнорируем, после релиза платежей дропаем.
|
||||
- **D32. Каталог — конфигурируемый (БД + админка).** Базовые ценности (атомы
|
||||
начисления): Фишки, подсказки, дни-без-рекламы, участие-в-турнире. **Продукт = набор
|
||||
атомов + цена** (по одной ценности или комбо). «Пакет Фишек» — цена **per-метод**
|
||||
(мультивалютная: Голоса/Stars/руб — один продукт, D2). «Ценность за Фишки» — цена в
|
||||
Фишках (единая). Курсы покупки Фишек и Фишки за rewarded — тоже в каталоге.
|
||||
- **D33. Стекинг «без рекламы»:** `paid_until[origin] += срок` от `max(now, текущий
|
||||
конец)` (остаток не теряется, «плюсуются» как в документе). «Навсегда» — отдельный
|
||||
вечный флаг (перекрывает сроки). Бонусы «(+50)» — маркетинговая пометка владельца;
|
||||
юзеру показываем финальную цифру, в модели просто количество.
|
||||
- **D34. Деактивация, не удаление.** Продукты soft-delete (деактивация). Покупка хранит
|
||||
**снимок проданного** (состав атомов + цена на момент) → архив. Для истории, чеков,
|
||||
налоговой — покупка не зависит от дальнейших правок/деактивации каталога.
|
||||
- **D35. Антифрод rewarded — только серверный verify провайдера** на старте (без своего
|
||||
дневного потолка). Провайдер-абстракция позволит добавить лимиты позже.
|
||||
- **D36. Email/direct.** У гостей раздел «Кошелёк» **скрыт полностью**; баланс и
|
||||
покупки — **только у durable-аккаунтов**. В direct email обязателен **перед первой
|
||||
покупкой** (якорь восстановления доступа; в VK/TG якорь — сама vk/tg-привязка). Флоу
|
||||
email уже есть (запрос кода → подтверждение → `ClearGuest` → durable).
|
||||
- **D37. Reaper — гостей чистит свободно:** у гостя баланса нет by design (Кошелёк
|
||||
скрыт, покупок нет, direct-rewarded нет). Спец-защита не нужна.
|
||||
- **D38. Гостевые ограничения — ОТДЕЛЬНЫЙ этап монетизации** (воронка к регистрации).
|
||||
Набор: макс **1 активная игра со случайным + 1 vs_ai**; **серверный** guest-гейт на
|
||||
friend-request / redeem-code / invitation-create. Находка: сейчас гейт **только в UI**
|
||||
— `social/friends.go:50` (`SendFriendRequest`), `robotfriends.go:41` (`RequestInGame`),
|
||||
`friendcodes.go:67` (`RedeemFriendCode`), `lobby/invitations.go:208`
|
||||
(`CreateInvitation`) **не проверяют `is_guest`**. Это изменение поведения игры → свой
|
||||
этап со своими тестами.
|
||||
- **D39. Хранение — неизменяемый журнал + материализованный баланс.** Журнал операций
|
||||
**append-only** (только INSERT, никогда UPDATE/DELETE — полный аудит, №3). Баланс
|
||||
сегмента `(account, source)` и бенефиты `(account, origin)` — быстрый материализованный
|
||||
кэш, обновляется **в той же транзакции**, что и запись журнала; пересчитывается из
|
||||
журнала для сверки.
|
||||
- **D40. Финансовый отчёт per-user в `/_gm`** — балансы сегментов, платежи, траты,
|
||||
гранты, возвраты, полная история — расширение существующей карточки
|
||||
(`UserDetailView`, `handlers_admin_console.go:343`). Плюс экспорт журнала (D27).
|
||||
- **D41. Налоги/чеки — авто через провайдера.** **Robokassa** (direct) — авточек НПД
|
||||
(режим самозанятого). **VK** — сам процессит Голоса через налоговую (владельцу делать
|
||||
нечего). **TG Stars** — налоговой стороны нет (для РФ-самозанятого невыводимы легально
|
||||
= не доход по НПД; принимаем, чек не формируем). ОРД по VK-рекламе — на стороне VK.
|
||||
*(Не юридическая консультация — точную схему НПД владелец сверяет с налоговой стороной.)*
|
||||
|
||||
## Заметки к оформлению документов
|
||||
|
||||
- **Язык документов (решено).** `PAYMENTS.md` — **на английском**, терминами как в коде
|
||||
и общепринятыми (`ledger`, `refund`, `idempotency`, `order`, …). Рядом
|
||||
`PAYMENTS_ru.md` — перевод на простой русский владельца. Паттерн `FUNCTIONAL.md` +
|
||||
`FUNCTIONAL_ru.md`, уже принятый в репозитории; мирроринг правок — в том же PR.
|
||||
- **Язык диалога** с владельцем — по-русски, без калек-англицизмов
|
||||
(«леджер»→«журнал операций»). Сохранить как feedback-память после выхода из plan mode.
|
||||
- **Формат интервью (владелец флагнул дважды).** ВСЕ вопросы владельцу — только через
|
||||
интерактивное интервью (AskUserQuestion). НИКОГДА не выносить вопросы в текст ответа,
|
||||
даже «мелкие» или «да/нет»: смешение текстовых и интерактивных вопросов раздражает и
|
||||
ведёт к пропускам. Текст — только для фиксации решённого и пояснений. Усилить
|
||||
feedback-память `prefer-interview-mode` после plan mode.
|
||||
|
||||
## Все развилки закрыты (D1-D41)
|
||||
|
||||
Интервью завершено. Дальше — оформление документов и реализация по релизам.
|
||||
|
||||
## План внедрения (черновик PLAN.md — «слоями»)
|
||||
|
||||
Владелец выбрал слоёную стратегию: сначала вся механика без реальных денег (обкатка
|
||||
через `admin_grant`), затем монетизация (все рельсы + реклама вместе).
|
||||
|
||||
### Релиз 1 — механика без денег (обкатка через `admin_grant`)
|
||||
|
||||
- **E0. Фундамент данных `payments`.** Схема + DB-роль + jetgen-таргет + миграции.
|
||||
Таблицы: журнал операций (append-only, D39), балансы сегментов `(account, source)`,
|
||||
бенефиты `(account, origin)` (paid_until/forever + подсказки count), каталог продуктов
|
||||
(D32, soft-delete D34), заказы `orders` (D22), `payment_events` (D26). Доменный пакет с
|
||||
жёсткой границей (D3). Старт deprecate `hint_balance`/`paid_account` (expand).
|
||||
- **E1. Доверенный сигнал платформы.** Platform в сессии (kind+подтип), переподтверждение
|
||||
на холодном старте (VK sign / TG `initData`), fail-closed (D17-D18); gateway прокидывает
|
||||
`platform` в backend.
|
||||
- **E2. Ядро валюты + бенефитов.** Баланс Фишек; трата Фишек → бенефит атомарно (D7);
|
||||
гейт по контексту (D6-D8); применение per-origin (без рекламы стекинг D33, подсказки
|
||||
D12); переписать `SpendHint` на контекст-аварное списание; миграция/обнуление legacy
|
||||
(D13, D31). Обкатка начислений через `admin_grant` (D16).
|
||||
- **E3. Кошелёк UI.** Раздел в `SettingsHub` (между Друзья/Инфо), балансы + бенефиты
|
||||
(минимальный, D24), витрина каталога, скрыт у гостей (D36), GP-заглушка (D19),
|
||||
предупреждение при трате vk/tg в вебе (D10).
|
||||
|
||||
### Релиз 2 — монетизация (рельсы + реклама вместе)
|
||||
|
||||
- **E4. Durability (PITR).** WAL-архив (pgBackRest/WAL-G, D4) — ДО первого реального
|
||||
приёма денег.
|
||||
- **E5. Приём платежей.** Order-flow (D22); единый payments-домен принимает колбэки
|
||||
(D21); Robokassa + VK + TG (бот-outbox на SQLite, D25); идемпотентность; `payment_events`
|
||||
+ диспетчер уведомлений (D26); чеки НПД (D41); внешние + ручные возвраты (D27).
|
||||
- **E6. Реклама.** VK interstitial (частота-конфиг D30) + VK rewarded (verify→Фишки D29,
|
||||
D35); `ads.Eligible` расширение per-origin (D31). Провайдер-абстракция (D28).
|
||||
- **E7. Админка / отчёты.** Финансовый отчёт per-user в `/_gm` (D40); UI ручного
|
||||
возврата; экспорт журнала (D27).
|
||||
|
||||
### Отдельный этап (изменение поведения игры)
|
||||
|
||||
- **E8. Гостевые ограничения.** Серверный guest-гейт друзей/приглашений + лимиты игр
|
||||
(1 случайная + 1 vs_ai, D38). Свои тесты; можно вести параллельно.
|
||||
|
||||
### Будущее
|
||||
|
||||
- **E9. Турнирный взнос.** Тип-ценность заложен в E0; механика позже.
|
||||
|
||||
## Финальные артефакты
|
||||
|
||||
1. `PAYMENTS.md` (англ) + `PAYMENTS_ru.md` (рус) — **первыми**, из Decisions Log D1-D41;
|
||||
поддерживать далее (мирроринг в том же PR, как FUNCTIONAL).
|
||||
2. `PLAN.md` — из раздела «План внедрения» выше, с критериями готовности на этап.
|
||||
3. Реализация по релизам, начиная с E0.
|
||||
|
||||
## Verification
|
||||
|
||||
Каждый этап — своим слоем (`docs/TESTING.md`): **unit** (гейт по контексту, стекинг
|
||||
сроков, курсы, идемпотентность-ключи); **integration** (Postgres-backed атомарные
|
||||
транзакции «Фишки↔бенефит», идемпотентность колбэков, бот-outbox доставка); **UI**
|
||||
(Кошелёк, предупреждения, гость-скрыт, GP-заглушка); контурный прогон на `development`
|
||||
перед промоушеном. Локальная полная проверка перед пушем (интеграция + UI + codegen).
|
||||
Прод: expand-contract миграции (rollback-safe), PITR армирован до первого приёма денег.
|
||||
Комплаенс-гейт — обязательная регрессия (direct-бенефит не активируется в VK/TG).
|
||||
@@ -275,6 +275,16 @@ INSERT** (никогда UPDATE/DELETE — полный аудит). Балан
|
||||
бенефиты `(account, origin)` — быстрый **материализованный** кэш, обновляется **в той же
|
||||
транзакции**, что и запись журнала, и пересчитывается из журнала для сверки.
|
||||
|
||||
**In-process кэш чтения.** Поверх материализованных таблиц пакет payments держит
|
||||
in-process кэш сегментов и бенефитов по ключу-аккаунту (write-through), чтобы горячие пути
|
||||
чтения — проверка показа рекламы, доступность подсказок, экран кошелька, гейт траты — на
|
||||
устоявшемся пути **не** делали ни одного запроса к схеме `payments`. Кэш инвалидируется на
|
||||
каждой изменяющей операции payments (трата / грант / пополнение / возврат / мерж) и
|
||||
перечитывается из материализованных таблиц при промахе (тот же write-through-паттерн, что и у
|
||||
гейта блокировок аккаунта). Один инстанс — под текущий деплой; многоинстансный backend
|
||||
потребовал бы общего кэша. Присутствие identity (какие сегменты «не спят», §6) передаёт
|
||||
вызывающий, здесь не кэшируется, поэтому отвязка/повторная привязка действует сразу.
|
||||
|
||||
**Награждение админом.** Админ начисляет **только конкретные ценности** (без рекламы /
|
||||
подсказки) — **никогда не Фишки** (подаренный баланс валюты = обход кассы стора). Админ
|
||||
**выбирает origin** при выдаче (ответственность за комплаенс на нём: `origin=vk`
|
||||
|
||||
Reference in New Issue
Block a user