Budget Control & Luck Pool
1. Summary
Goal: Контроль экономики крафтов и поощрение активных игроков через систему буста вероятностей.
User Value: Активные игроки (50%+ прогресс крафта) получают × 3-12.9 увеличение шансов на материалы для целевых скинов через Boost.
2. Business Logic
Budget Periods
Экономика разбита на периоды для контроля расходов. Параметры настраиваются per-season через Setup Wizard (Step 2 — Budget):
| Параметр | Default | Per-season |
|---|---|---|
| Количество периодов | 9 (auto: ceil(durationDays / 10)) | budgetPeriodsCount (1-30) |
| Бюджет периода | 3333₽ (333₽/день × 10) | budgetTotalRub / periodsCount |
| Общий бюджет | 333₽ × durationDays (auto) | budgetTotalRub (1000-10M, optional override) |
| Перенос остатков | Включён | budgetCarryoverEnabled (toggle) |
- Внутри месяца: остаток переносится в следующий период (если
budgetCarryoverEnabled = true) - В конце месяца (каждые 3 периода): остаток сгорает
- Carryover можно отключить per-season через Setup Wizard
Luck Pool
Механика поощрения активных игроков через увеличение вероятностей.
Вкратце: Игроки с ≥50% прогрессом крафта получают буст × 3-12.9 к материалам.
См. Luck Pool для полного описания механики, seniority, anti-abuse, edge cases.
Budget Exhausted
Штрафной множитель: × 0.01 (99% снижение вероятности)
Применяется ко всем FRAGMENT/BLUEPRINT с targetSkinId (все пользователи, не только участники Luck Pool). Остальные награды не затрагиваются.
Определение: available = baseBudget + carriedOver - spent <= 0
Уведомление: Админ получает alert только при переходе из "хватает" в "не хватает".
Игроки могут крафтить даже при исчерпанном бюджете. Система только логирует траты для администратора.
Manual Budget Lock
Экстренная кнопка администратора — принудительно переводит все кейсы и спины в режим исчерпанного бюджета (снижает шансы FRAGMENT/BLUEPRINT), независимо от реального состояния бюджета.
| Параметр | Решение |
|---|---|
| Тип | Мягкая блокировка — снижает вес FRAGMENT/BLUEPRINT (= автоматическое budget exhaustion) |
| Покрытие | Все кейсы (free, paid, streak points) + все спины текущего сезона |
| Гранулярность | Пер-сезон — привязана к активному сезону |
| UX пользователя | Тихо — ничего не показывает, просто реже выпадают редкие предметы |
| Деактивация | Только ручная, без таймера |
| Причина | Опциональное текстовое поле при активации (до 500 символов) |
Механизм: BudgetService.isBudgetExhausted(seasonId) — единственная точка входа для обоих case-opening.service.ts и user-spin.service.ts. Проверка Season.isManualBudgetLock добавлена первой в этом методе — оба сервиса автоматически получают нужное поведение без изменений.
Ручная блокировка имеет приоритет над автоматическим расчётом бюджета. Если isManualBudgetLock = true, isBudgetExhausted() возвращает true независимо от остатка бюджета.
Панель ручной блокировки доступна через Ctrl+Alt+M в BudgetStep (Step 2 Season Setup Wizard, ACTIVE mode).
Когда блокировка активна — рядом с заголовком появляется маленькая красная точка-индикатор.
Admin Notifications (TG Alerts)
Budget Control отправляет уведомления администраторам через Telegram при критических событиях:
| Событие | Тип | Когда отправляется | Inline кнопки |
|---|---|---|---|
| Бюджет исчерпан | BUDGET_EXHAUSTED | При первом исчерпании бюджета периода | +500₽, +1000₽, +2000₽, +5000₽, Открыть бюджет |
| Подозрительная активность | SUSPICIOUS_CRAFTS | При >2 крафтах за 24ч одним пользователем | Проверить пользователя |
| Смена периода | PERIOD_TRANSITION | При автоматическом переходе между периодами | Открыть бюджет |
| Блокировка активирована | BUDGET_LOCK_ACTIVATED | При ручной активации блокировки администратором | 👀 Открыть бюджет |
| Блокировка снята | BUDGET_LOCK_DEACTIVATED | При ручной деактивации блокировки администратором | 👀 Открыть бюджет |
Формат уведомлений
BUDGET_EXHAUSTED:
⚠️ Бюджет исчерпан!
Сезон: {seasonNumber}
Период: {periodNumber}
День периода: {dayOfPeriod}
Буст отключен до следующего периода.
[+500₽] [+1000₽] [+2000₽] [+5000₽]
[Открыть бюджет]
SUSPICIOUS_CRAFTS:
🚨 Подозрительная активность!
Пользователь: {username || userId}
Крафтов за 24ч: {craftsIn24h}
Порог: {threshold}
[Проверить пользователя]
PERIOD_TRANSITION:
📅 Смена периода
Сезон: {seasonNumber}
Переход: период {fromPeriod} → {toPeriod}
Перенесено: {budgetCarriedOver}₽
Сгорело: {budgetExpired}₽
[Открыть бюджет]
BUDGET_LOCK_ACTIVATED:
🔒 Блокировка выпадения АКТИВИРОВАНА
Сезон: #{seasonNumber}
Администратор: {adminName}
Время: {datetime} UTC
📝 Причина: {reason} ← если указана
Выпадение редких предметов (FRAGMENT/BLUEPRINT) ограничено до снятия блокировки.
BUDGET_LOCK_DEACTIVATED:
🔓 Блокировка выпадения СНЯТА
Сезон: #{seasonNumber}
Администратор: {adminName}
Время: {datetime} UTC
Выпадение редких предметов восстановлено в штатный режим.
Кнопки admin:budget_add_{amount} обрабатываются в backend/src/domains/telegram/handlers/admin-callback.handler.ts и вызывают BudgetAdminService.incrementBudget().
Edge Cases
| Ситуация | Поведение |
|---|---|
| Бюджет исчерпан | Штраф × 0.01 ко всем крафтовым материалам (FRAGMENT/BLUEPRINT) |
| Конец месяца | Остаток бюджета сгорает |
| Конец периода | Остаток переносится в следующий период |
| Крафт в сезоне A, вывод в сезоне B | spentRub учтён в сезоне A (по крафту). Withdrawal не привязан к сезону — это delivery, не commitment |
| Ручная блокировка активна | isBudgetExhausted() возвращает true независимо от остатка бюджета периода |
| Carryover отключён per-season | budgetCarryoverEnabled = false → остаток не переносится между периодами (ни внутри месяца, ни в конце) |
budgetTotalRub = null | Авто-расчёт: DAILY_BUDGET_RUB × durationDays (default поведение) |
Edge cases связанные с пулом удачи (вход, выход, seniority) описаны в Luck Pool.
3. ADR (Architectural Decisions)
Архитектурные решения по Luck Pool (буст, seniority) описаны в Luck Pool ADR.
Почему per-season конфигурация бюджета?
Проблема: Бюджетные параметры (333₽/день, 10-дневные периоды, 9 периодов, carryover enabled) были глобальными константами — одинаковыми для всех сезонов. Каждый сезон может иметь разную длительность и требовать разный бюджет.
Решение: Три новых поля в Season model:
budgetPeriodsCount(Int, default 9) — количество периодовbudgetTotalRub(Int?, null = auto) — override общего бюджетаbudgetCarryoverEnabled(Boolean, default true) — переключатель carryover
Настраиваются через Step 2 (Budget) в Season Setup Wizard до активации сезона.
Альтернативы (отклонены):
- Отдельная таблица
BudgetConfig— избыточно для 3 полей - Оставить глобальные константы — не позволяет варьировать бюджет между сезонами
Последствия: Гибкость per-season, глобальные константы (BUDGET_CONTROL) остаются как defaults. createSeasonPeriods() принимает periodsCount из Season model. handlePeriodTransition() читает budgetCarryoverEnabled из Season.
Почему Budget не блокирует крафты?
Проблема: При блокировке крафтов игроки потеряют прогресс.
Решение: Budget — инструмент мониторинга, а не ограничения.
Последствия:
- Админ контролирует экономику через отмену выводов
- Игроки не теряют прогресс из-за бюджета
Почему поля блокировки в Season, а не BudgetPeriod?
Проблема: Где хранить состояние ручной блокировки — в BudgetPeriod или Season?
Решение: Поля isManualBudgetLock, budgetLockActivatedAt, budgetLockActivatedBy, budgetLockReason хранятся в модели Season.
Альтернативы (отклонены):
- BudgetPeriod — отклонён: блокировка пер-сезон, а не пер-период. При смене периода блокировка должна сохраняться
- Отдельная таблица BudgetLock — отклонён: избыточно для 4 полей, лишний JOIN
Последствия: Блокировка переживает transitions между периодами. Один findUnique по Season вместо дополнительного JOIN.
Почему isBudgetExhausted() — единственная точка входа?
Проблема: Блокировка должна одновременно действовать на кейсы (case-opening) и спины (user-spin).
Решение: BudgetService.isBudgetExhausted(seasonId) — единственная функция, которую вызывают оба сервиса. Добавление проверки isManualBudgetLock в неё автоматически покрывает оба пути без изменений в consumers.
Последствия: Минимальные изменения (1 файл для core логики), нулевой риск рассинхронизации между кейсами и спинами.
Почему расход фиксируется при крафте, а не при выводе?
Проблема: Крафт и вывод могут произойти в разных сезонах. Игрок крафтит скин в последний день сезона A, но выводит уже в сезоне B.
Решение: Budget трекает commitment (обязательство платформы), а не delivery (фактическую доставку).
Аналогия: Бухгалтерский "accrued expense" — расход фиксируется когда возникает обязательство, а не когда деньги уходят.
Последствия:
spentRub= сумма обязательств платформы за периодWithdrawalне имеетperiodId— это operational detail- Админ контролирует реальные расходы через отмену выводов
- Скин в инвентаре = платформа уже "должна" его игроку
4. Architecture
Integration Overview
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| BudgetService | backend/src/domains/budget/services/budget.service.ts | Core: статус, запись расходов, проверки |
| BudgetAdminService | backend/src/domains/budget/services/budget-admin.service.ts | Admin: increment/decrement/reset бюджета, ручная блокировка |
| BudgetLifecycleService | backend/src/domains/budget/services/budget-lifecycle.service.ts | Lifecycle: создание периодов, transitions |
| LuckPoolService | backend/src/domains/luck-pool/services/luck-pool.service.ts | Управление пулом и бустом |
| BudgetOperationsService | backend/src/domains/budget/services/budget-operations.service.ts | Unified view операций (крафты + выводы) для админки |
| WithdrawalAdminService | backend/src/domains/budget/services/budget-withdrawal.service.ts | Отмена выводов администратором |
| BudgetRepository | backend/src/domains/budget/repositories/budget.repository.ts | CRUD для BudgetPeriod |
| Constants | shared/src/constants/budgetControl.ts | Константы-defaults (overridable per-season через Season model) |
Константы системы (defaults, overridable per-season)
BUDGET_CONTROL = {
PERIOD_DAYS: 10,
PERIODS_PER_MONTH: 3,
DAILY_BUDGET_RUB: 333,
PERIOD_BUDGET_RUB: 3333,
MIN_PROGRESS_FOR_POOL: 0.5,
INACTIVE_DAYS_THRESHOLD: 14,
POOL_CHECK_INTERVAL_HOURS: 12,
BASE_BOOST_MULTIPLIER: 3.0,
SENIORITY: {
MULTIPLIER_PER_PERIOD: 1.2,
MAX_ACTIVE_PERIODS: 9,
},
BUDGET_EXHAUSTED_MULTIPLIER: 0.01,
BUDGET_CARRYOVER: {
ENABLED: true,
WITHIN_MONTH_ONLY: true,
},
BLOCKED_SKINS: {
CLEAR_INTERVAL_PERIODS: 3,
},
ACTIVITY_ACTIONS: ['CASE_OPENED', 'SPIN_USED', 'STREAK_BONUS_CLAIMED'],
ADMIN: {
BUDGET_INCREMENT_PRESETS: [500, 1000, 2000, 5000],
BUDGET_INCREMENT_MIN: 100,
BUDGET_INCREMENT_MAX: 10000,
BUDGET_DECREMENT_PRESETS: [500, 1000],
BUDGET_DECREMENT_MIN: 100,
SUSPICIOUS_CRAFTS_THRESHOLD: 2,
},
}
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| BudgetPeriod | Период бюджета | baseBudgetRub, carriedOverRub, spentRub, isActive |
| LuckPoolEntry | Участник пула | activePeriods, boostMultiplier, isActive, blockedSkinIds |
| CraftBudgetLog | Лог крафтов | costRub, hadBoost, seniorityMultiplier, totalBoostMultiplier |
| Season (budget fields) | Конфигурация + ручная блокировка | budgetPeriodsCount, budgetTotalRub, budgetCarryoverEnabled, isManualBudgetLock, budgetLockActivatedAt, budgetLockActivatedBy, budgetLockReason |
Relationships
6. API Endpoints
- Budget Admin
- Luck Pool Admin
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/budget/status | Статус текущего периода |
| GET | /admin/budget/periods | Все периоды сезона |
| POST | /admin/budget/periods/:periodId/increment | Увеличить бюджет |
| POST | /admin/budget/periods/:periodId/decrement | Уменьшить бюджет |
| POST | /admin/budget/periods/:periodId/reset | Сбросить бюджет к значению по умолчанию |
| GET | /admin/budget/craft-logs | Логи крафтов |
| GET | /admin/budget/operations | Unified список операций (крафты + выводы) |
| GET | /admin/budget/operations/pending-count | Количество ожидающих выводов (для badge) |
| POST | /admin/budget/withdrawals/:id/cancel | Отменить вывод (только PENDING/PROCESSING) |
| POST | /admin/budget/lock | Активировать ручную блокировку (body: { reason?: string }) |
| DELETE | /admin/budget/lock | Снять ручную блокировку |
Luck Pool API описан в Luck Pool.
7. Related
- Luck Pool — полная документация по пулу удачи
- Cases — использует Luck Pool для буста материалов
- Daily Spins — использует Luck Pool для буста материалов
- Craft — триггерит выход из пула и записывает расходы
- Seasons — контекст для периодов и пула