Управление промо и бонусами на уровне backend
Полный текст статьи
1) Зачем выносить промо в отдельный backend
Денежные инварианты. Бонус ≠ «приплюсовали баланс»: это контракт с условиями (вейджер, вклад по играм, максимум ставки/выигрыша).
Скорость изменений. Команды маркетинга выпускают кампании ежедневно — нужен декларативный движок правил и откат.
Анти-абьюз/комплаенс. KYC/RG/AML, velocity, сегментация, таски «четырёх глаз» на дорогие офферы.
Наблюдаемость и отчётность. SLO, стоимость промо, влияние на GGR/NGR/LTV.
Принцип: промо-ядро — отдельный сервис с собственными статусными машинами, а деньги двигаются только через кошелёк, идемпотентно.
2) Типология бонусов и инварианты
Deposit match (100% до X): начисляется после capture депозита, вейджер X×.
Cashback (проигрыш-бэк): рассчитывается по окну времени/играм, может быть sticky/non-sticky.
Free Spins / Free Bets: купоны/токены с прайсом за спин/ставку, фиксированный RTP-пул.
Квесты/миссии: задание → прогресс → награда.
Турниры/рейс-ивенты: вклад событий, рейтинг, призовые.
Инварианты:- Sticky: нельзя вывести до выполнения условий.
- Max bet / Max win: лимиты на ставку/выплату из бонусных средств.
- Contribution: вклад по играм (например, slots=100%, live=10%).
- Expiry: срок действия бонуса и окна вейджера.
3) Архитектура сервиса бонусов
Admin (кампании/правила) ─Promo API ─Rules Engine / Eligibility
│
├─Bonus Ledger (состояние офферов)
├─Wagering Engine (прогресс)
├─Anti-Abuse (лимиты/фрод/velocity)
└─Outbox (events) ─Kafka/Pulsar ─BI/DWH/CRM
Wallet/Ledger── Idempotent Commands ───┘Rules Engine — декларативные условия (сегменты, гео/лицензия, каналы, KYC/RG).
4) Модель данных (упрощённо)
`bonus_grant`
`wager_progress`- `grant_id, required_minor, contributed_minor, remaining_minor, last_update_at`
- `schema_id, rules: [{game_type:"slot", pct:100},{game_type:"live", pct:10}]`
`bonus_ledger_entry` (аудит)
5) Статусные машины и саги
5.1 Выдача (issue) — сага
1. eligibility.check (сегмент, RG/KYC, velocity)
2. grant.create (status=`issued`)
3. wallet.credit[bonus] (идемпотентно; при sticky — в бонусный суб-баланс)
4. activate (status=`active`)
5. emit `bonus.issued`
Rollback: при падении на 3 шаге → `grant.cancel` + событие `bonus.revoked`.
5.2 Прогресс вейджера
На `bet.settled` считать вклад = `stake_minor contribution_pct` (или по win/loss правилам).
Обновлять `wager_progress` атомарно; при достижении 100% — `complete`.
5.3 Завершение (consume)
complete → `wallet.convert_bonus_to_cash` (если non-sticky) или снятие ограничений на вывод.
emit `bonus.consumed`.
5.4 Истечение/отзыв
По `expires_at` или правилу фрода → `revoke` (идемпотентно), возможна компенсация согласно политике.
6) Контракты с кошельком (только через API, всегда идемпотентно)
Начислить бонус
POST /v1/wallet/credit
Headers: X-Idempotency-Key: bonus_grant_123
{
"player_id":"p_001",  "amount":{"minor_units":100000,"currency":"EUR"},  "balance_type":"bonus",  "reference":{"grant_id":"gr_123","offer_id":"of_777"}
}
→ 200 {"status":"credited","entry_id":"e_9001"}Конвертация в кэш после выполнения условий
POST /v1/wallet/convert
Headers: X-Idempotency-Key: bonus_convert_gr_123
{
"player_id":"p_001",  "from_balance":"bonus",  "to_balance":"cash",  "amount_minor":100000,  "reference":{"grant_id":"gr_123"}
}
→ 200 {"status":"converted","entry_id":"e_9010"}- запрос `bets.authorize` отклоняется кодом `BONUS_MAX_BET_EXCEEDED`.
7) API сервиса промо (эталоны)
Создать оффер (админ)
POST /v1/offers
{
"name":"Welcome 100% up to 100€",  "type":"deposit_match",  "params":{"match_pct":100,"cap_minor":10000,"wager_x":20,"sticky":true,       "max_bet_minor":200,"max_win_minor":50000,"contribution_schema_id":"c_slot100_live10"},  "eligibility":{"brands":["A"],"regions":["EU"],"segment":"new_depositors"},  "schedule":{"start":"2025-10-20T00:00:00Z","end":"2025-11-30T23:59:59Z"}
}
→ 201 {"offer_id":"of_777"}Выдать бонус (runtime)
POST /v1/bonus/grants
Headers: X-Idempotency-Key: grant_p001_of777
{
"player_id":"p_001","offer_id":"of_777","trigger":"deposit_captured","amount_minor":10000
}
→ 200 {"grant_id":"gr_123","status":"active"}Прогресс вейджера (read)
GET /v1/bonus/grants/gr_123/progress
→ 200 {"required_minor":200000,"contributed_minor":45000,"remaining_minor":155000,"pct":0.225}Аннулировать/отозвать
POST /v1/bonus/grants/gr_123/revoke
Headers: X-Idempotency-Key: revoke_gr_123
{ "reason":"fraud_velocity" }
→ 200 {"status":"revoked"}Все write-вызовы — с `X-Idempotency-Key` и `X-Trace-Id`.
8) Анти-абьюз и комплаенс
Velocity лимиты: выдач/конверсий/попыток депозита (Redis counters + TTL + Lua).
Дедуп триггеров: один депозит → один grant по правилу.
Сегментация и RG: исключить self-excluded/лимитных; per brand/region лицензии.
Блок конфликта офферов: одновременно активен только один welcome-бонус; приоритеты.
Детектор аномалий: множественные аккаунты/устройства/ASN, быстрые «обнуления» вейджера.
«Четыре глаза» на крупные гранты и ручные корректировки.
WORM-аудит всех изменений правил/грантов/конверсий.
9) Наблюдаемость, метрики и SLO
SLO (ориентиры):- `grant.issue p95` (issue→credited) ≤ 300–500 мс.
- Обновление `wager_progress p95` ≤ 200 мс с момента `bet.settled`.
- События `bonus.` в шине p95 ≤ 2 мин от произошедшего.
- «Потерянных/дублированных грантов/конверсий» = 0.
- Rate/latency по `issue/convert/revoke`, error-rate (business/4xx/5xx), `IDEMPOTENCY_MISMATCH`.
- Конверсия вейджера, средний `time-to-complete`, доля просроченных.
- Стоимость промо: `promo_cost` (minor) и `promo_roi` на когортах.
- Анти-абьюз: срабатывания velocity, отклонённые max bet/win.
Трейсинг: OpenTelemetry по цепочке `trigger → grant → wallet.credit → progress.update → convert`.
10) Интеграция с RGS/играми
Купоны Free Spins/Free Bets — через `entitlements` API: выдача токенов, списание в рантайме, телеметрия по использованию.
Max bet/win — правила в `bets.authorize` и `bets.settle`; возвращать коды `BONUS_RULE_VIOLATION`.
Contribution — схема на уровне `bet.settled` (по `game_type/provider_id`), версияция схем.
11) DWH/BI и отчёты
Outbox события → Lake (bronze) → Silver (дедуп, SCD2) → Gold витрины:- `fact_bonus_grants`, `fact_wager_progress`, `fact_bonus_cost`, `fact_promo_roi`.
- SLA свежести: Silver ≤ 15 мин, Gold ≤ 30–60 мин.
- Панели: конверсия по офферам/сегментам, time-to-complete, вклад по играм, абьюз-инциденты.
12) Безопасность и резидентность
mTLS + OAuth2 CC; scope’ы `promo:issue`, `promo:convert`, `promo:revoke`.
Ключи/токены — per brand/region, краткоживущие; секреты в Vault/HSM.
PII-изоляция: `player_id` — псевдоним; RLS по `brand/region`.
Rate limits и квоты на выдачи; защита от штормов ретраев.
13) Чек-листы
Платформа/оператор
- Все денежные операции идут через Wallet с `Idempotency-Key`.
- Rules/Eligibility версионируются; «двойное письмо» событий на миграциях.
- Contribution-схемы централизованы, покрыты тестами.
- Velocity и анти-фрод включены; «четыре глаза» на крупные суммы.
- Outbox/CDC, DLQ и управляемый replay для `bonus.`.
- SLO-дашборды, OpenTelemetry, WORM-аудит.
- DWH-витрины для ROI и комплаенса (RG/AML).
Интеграции (RGS/кошелёк/CRM)
- Проверяю max bet/win; возвращаю код бизнес-ошибки.
- Прокидываю `trace_id` и `idempotency_key`.
- Дедуп триггеров и гарантии доставок (webhooks подписаны).
14) Красные флаги (анти-паттерны)
Начисление бонуса «вручную» прямо в баланс, минуя Wallet.
Отсутствие идемпотентности → двойные гранты/конверсии.
Вейджер считается по `bet.placed`, а не по итогам `bet.settled`.
Нет contribution-схем или они «зашиты» в коде провайдеров.
Конфликтующие офферы активируются одновременно.
Нет velocity/анти-фрода и WORM-аудита.
События `bonus.` публикуются в обход outbox/CDC.
Показатели промо не сходятся с Ledger/BI (нет витрин ROI).
15) Итог
Надёжный backend промо — это контракты и инварианты, а не «прибавить баланс». Он отделяет правила от денег, считает прогресс по фактическим исходам, гарантирует идемпотентность и наблюдаемость, защищает от абьюза и обеспечивает комплаенс. С таким ядром маркетинг двигается быстро, игрок видит честные условия, а финансы и регуляторы получают точную картину стоимости и эффекта каждого оффера.
