# Монетизация 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`). **АМЕНД (E6, находка владельца по ToS):** VK iOS — **заморозка только ПОКУПОК** (деньги→Фишки), а не траты. Apple запрещает там лишь **покупать** внутриигровые ценности за любую валюту; а **тратить** Фишки VK-кошелька (заработанные rewarded-рекламой или купленные на том же VK-аккаунте в Android) и зарабатывать их — легально и на iOS. Код: `vkFrozen()` гейтит только `CreateOrder` (покупку), не `spendableSources` (трату). - **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. Не гасится «без рекламы». Сколько Фишек за просмотр — в блоке экономики. **АМЕНД (E6, по факту реализации):** у VK Mini App серверного verify-колбэка **нет** — `VKWebAppShowNativeAds` отдаёт только клиентский `data.result`. Поэтому начисление **client-attested**, а защита (и экономический рычаг) — **серверный дневной и часовой кап** (config `reward_daily_cap` / `reward_hourly_cap`, дефолт 50 / 10) + идемпотентность по клиентскому nonce. Сеть с серверным verify встроится за ads-абстракцией позже. - **D30. Частота навязанного interstitial** (конфигурируемые серверные значения): глобальный кулдаун **на юзера сквозь все партии**, дефолт **5 мин**; **vs_ai — 30 мин** (соосно кулдауну подсказок). Применение **подсказки** триггерит ролик после хода **независимо** от основного кулдауна, со своим кулдауном **1 мин**. Оффлайн — только баннер. Уважать собственные лимиты частоты VK. **АМЕНД (E6, по факту реализации):** гейт **зеркальный** — сервер отдаёт кулдауны и `suppressed` в профиле (`adsFor`), клиент сам гейтит по **единому** времени последнего показа в `localStorage` (без раунд-трипа на ход). Таймер **общий на все виды** — вид (`hint`/`move`/`vs_ai`) лишь выбирает нужный интервал, поэтому «независимость» подсказочного кулдауна значит лишь **более короткий интервал от последнего показа**, а не отдельный таймер: hint-ролик и move-ролик не встают подряд (баг раздельных таймеров: после hint-ролика move-таймер оставался нулевым → следующий ход сразу крутил рекламу). Ролик показывается **только после подтверждённого хода или подсказки** — **не** после пропуска, обмена или сдачи (за не-очковое действие рекламой не «награждаем»). Interstitial — **только VK** (как и rewarded). Частота — глобальная на все игры (localStorage на устройство). - **D31. `paid_account` тоже deprecated** → удаление из схемы (как `hint_balance`), в пользу per-origin бенефитов «без рекламы». Существующий `ads.Eligible` (`backend/internal/ads/ads.go:107`) расширяется: баннер гасится по origin-бенефиту, **применимому в текущем контексте**, а не по одному глобальному флагу. Legacy `paid_account`/`hint_balance` в проде никем не выставлены (потока покупки не было) → обнуляем/игнорируем, после релиза платежей дропаем. **АМЕНД (E6, по факту реализации — expand-contract, шаг 1 «contract-код»):** доменное использование обеих колонок **убрано** — поля `Account.HintBalance` / `Account.PaidAccount`, их скан, мёртвый `account.SpendHint`, `account.GrantHints` и админ-действие «grant-hints» (роут `/_gm/users/:id/grant-hints`, форма, `UserDetailView.HintBalance`/ `PaidAccount`); отображение подсказок в игре теперь всегда из payments (`HintsAvailable`), профильный баланс — из payments-бенефита. **Колонки БД пока оставлены** (без миграции — откат образа DB-safe); их `DROP` — отдельным contract-PR, когда E6 стабилен на проде. - **D32. Каталог — конфигурируемый (БД + админка).** Базовые ценности (атомы начисления): Фишки, подсказки, дни-без-рекламы, участие-в-турнире. **Продукт = набор атомов + цена** (по одной ценности или комбо). «Пакет Фишек» — цена **per-метод** (мультивалютная: Голоса/Stars/руб — один продукт, D2). «Ценность за Фишки» — цена в Фишках (единая). Курсы покупки Фишек и Фишки за rewarded — тоже в каталоге. (Ставка rewarded + дневной/часовой потолки — строка общего конфига `payments.config`, правится в секции «Rewarded ads» на странице каталога админки; это не продаваемый продукт-атом.) - **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) — фискальный чек по **54-ФЗ** через облачную кассу Robokassa (провайдер как фискальный агент): чек формирует касса, не банк (банковский слип об оплате — отдельный документ, не фискальный чек). Настраивается владельцем в ЛКК Robokassa (касса + ОФД + СНО). Код — опционально: слать детализированный `Receipt` + `Email` покупателя (берём подтверждённый email-якорь D36) → чек с точным названием пакета и ставкой по СНО; иначе — обобщённый дефолт-чек из кабинета. `Receipt` входит в подпись (`MerchantLogin:OutSum:InvId:Receipt:Пароль#1`, URL-кодируется), одна позиция влезает в текущий GET-redirect (POST-форма — фолбэк на длину URL). Источник чеков один — ИП/касса, независимо от числа магазинов Robokassa. **VK** — процессит Голоса через налоговую сам. **TG Stars** — вне рублёвого фискального контура (принимаем, чек не формируем). ОРД по VK-рекламе — на стороне VK. *(Не юрконсультация — схему по 54-ФЗ/СНО владелец сверяет с бухгалтером.)* - **D42. Direct-рельс — несколько магазинов Robokassa = маршрутизация merchant-аккаунтов по каналу при едином кошельке.** Кошелёк `direct` остаётся один. Отдельные магазины Robokassa (web, android; ios — позже) различаются только кредами / Return-URL / учётом и **все зачисляют в сегмент `direct`**. Модель кошельков, стенка трат и origin бенефитов не меняются. Магазин выбирается по трастовому сигналу канала (`X-Platform` вида `/`: `direct/web`, `direct/android`), а не по подделываемому клиентскому полю; неизвестный подтип → магазин `web` (безопасный дефолт). Зачисление от выбора магазина не зависит (всегда `direct`), поэтому ошибка маршрутизации влияет максимум на учёт, не на деньги. - **D43. Standalone-приложения (Android, iOS) — вход только по email; покупка требует email-якорь.** В нативной сборке доступны только guest + email: VK ID-логин (full-page redirect на `id.vk.com`) не возвращается в Capacitor, TG Login Widget в WebView ненадёжен — это уже действующая реальность сборки, не новое ограничение. Direct-покупка требует подтверждённого email-якоря (D36) — он же адрес фискального чека (D41); гость не покупает. Отдельный сегмент `apple` НЕ заводим: iOS-standalone — тот же `direct`-контекст, внешний гейт (Robokassa) фондирует единый `direct`. Сегмент `apple` со стенкой (по образцу vk/tg) понадобился бы только при Apple IAP (StoreKit) — отложено до решения по iOS; и identity-kind `apple→direct`, и отдельный сегмент — аддитивны, без риска, делаются на месте. - **D44. Канал платежа хранится на заказе для раздельного учёта/отчёта.** Заказ получает поле `shop` (web/android/…) — аддитивная колонка. Используется в финансовом отчёте (D40) для разбивки «из какого магазина/канала платёж». На зачисление и на сегмент кошелька не влияет (всегда `direct`, D42). ## Заметки к оформлению документов - **Язык документов (решено).** `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-D44) Интервью завершено. Дальше — оформление документов и реализация по релизам. **Дополнение 2026-07-14 (интервью с владельцем).** D41 ревизована (владелец переходит на ИП: 54-ФЗ через облачную кассу Robokassa вместо авточека НПД); добавлены D42-D44 — сплит `direct`-рельса Robokassa на магазины по каналу (web/android; ios позже) при едином кошельке `direct` и входе email-only в standalone-приложениях. Реализация — новый этап E10 в `PLAN.md`. ## План внедрения (черновик 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).