Skip to main content

Cases System

1. Summary

Goal: Механизм конвертации внутренней валюты (Scrap) в предметы для крафта. Игрок открывает кейсы, собирает компоненты, крафтит реальный скин и выводит в Steam.

User Value: Возможность получить реальный скин без денежных вложений, только за активность в приложении. Путь: Активность → Scrap → Кейсы → Предметы → Крафт → Вывод в Steam.


2. Business Logic

Types of Cases

Доступ: Бесплатный

Cooldown: Регулируется CaseType.cooldownHours (по умолчанию 24ч)

Tracking: Таймер привязан к User.lastDailyCase

Цель: Retention — причина заходить каждый день

Opening Mechanics

Алгоритм открытия (реализован в CaseOpeningService):

1. Validation

  • Кейс должен быть активен (isActive: true)
  • Проверка cooldown: now > lastDailyCase + cooldownHours

2. Payment Strategy

Порядок списания
  1. Проверка купона (user_free_case_opens) → если есть, списывается купон
  2. Списание валюты (Scrap/SP) → только если нет купона
  3. Всё в одной atomic transaction
Daily Case Detection

Система определяет daily кейсы через caseType.isDailyFree флаг. При isDailyFree = true:

  • Валидация в админке гарантирует priceScrap = 0
  • cooldownHours автоматически берётся из CaseType
  • При открытии: actualScrapPrice = 0 независимо от поля priceScrap (защита)
Техническая реализация
// CaseOpeningService.openCase()
const isDailyFree = caseData.caseType.isDailyFree;
const actualScrapPrice = hasFreeOpens || isDailyFree || isStreakPointsCase ? 0 : caseData.priceScrap;

Логика гарантирует что daily кейсы всегда бесплатны, даже если в БД ошибочно указана цена.

3. RNG & Boost

Weighted Random выборка: P(item) = weight / sum(weights)

Luck Pool увеличивает шансы FRAGMENT/BLUEPRINT для активных игроков (× 3-13).

4. Reward Granting

  • Предмет создаётся в инвентаре
  • Валютные награды (Scrap/XP) начисляются сразу на счёт

5. Analytics

  • Запись в CaseOpening с snapshot выпавшей награды

Edge Cases

Что видит пользователь (UI):

СитуацияUI поведение
❌ Баланс < ценыКнопка disabled, tooltip "Недостаточно скрапа"
⏱️ Cooldown активенКнопка disabled, текст "Доступен через 2ч 15м"
🎫 Есть купонКнопка "Открыть бесплатно", баланс не трогаем
Если ошибка всё-таки дошла до бекенда

При race condition или stale cache — Telegram.WebApp.showAlert() с локализованным сообщением + haptic feedback.

Что видит админ при создании/редактировании кейса:

СитуацияВалидацияError Message
❌ Daily type + price ≠ 0Блокируется"Daily free cases must have priceScrap = 0"
✅ Daily type + price = 0РазрешеноAuto-set cooldownHours из CaseType если не указан
❌ Изменить цену daily кейса на ≠ 0БлокируетсяВалидация проверяет и текущий тип
Защита на уровне сервиса

Валидация в AdminCaseService предотвращает создание некорректных daily кейсов, даже если админ попытается обойти UI. Defense in depth.

Backend Error Codes (для API/тестов)
КодHTTPСообщение пользователю
INSUFFICIENT_BALANCE400"Недостаточно Scrap для открытия кейса"
INSUFFICIENT_STREAK_POINTS400"Недостаточно Streak Points для открытия кейса"
COOLDOWN_ACTIVE400"Case is on cooldown. Try again in X minutes"
CASE_NOT_FOUND404"Кейс не найден"

Это defense in depth — фронт блокирует UI, бекенд защищает данные.


3. ADR (Architectural Decisions)

Почему Weighted Random, а не фиксированные шансы?

Проблема: Нужна гибкая настройка вероятностей без изменения кода.

Решение: Weighted Random с весами в БД (CaseReward.weight).

Альтернативы (отклонены):

  • Фиксированные проценты — сложнее балансировать при добавлении предметов
  • Pity system (гарантированные дропы) — усложняет логику, убивает чистый азарт

Последствия: Простота настройки, но требует внимательного баланса.

Почему атомарные транзакции?

Критично для консистентности

При открытии кейса нужно одновременно списать баланс И выдать награду. Если падает между операциями — inconsistency данных.

Решение: Prisma $transaction для atomic operations.

Последствия: Гарантированная консистентность, но блокировка записей на время транзакции.

Что именно блокируется?

PostgreSQL использует row-level locking — блокируются только строки, участвующие в транзакции (запись конкретного пользователя), а не вся БД.

Параллельная операцияБлокируется?
Другой юзер открывает кейсНет
Этот же юзер открывает второй кейсДа, ждёт

4. Architecture

Services Overview

Key Components

КомпонентПутьОписание
CaseOpeningServicebackend/src/domains/cases/services/case-opening.service.tsОркестратор открытия
CaseRepositorybackend/src/domains/cases/repositories/case.repository.tsСложные выборки
Routesbackend/src/domains/cases/routes/case.routes.tsUser API
Admin Routesbackend/src/domains/cases/routes/admin-case.routes.tsAdmin API
Schemasbackend/src/domains/cases/schemas/case.schemas.tsВалидация

5. Database Schema

Models

МодельОписаниеКлючевые поля
CaseКонфигурация кейсаprice, currencyType, caseTypeId, isActive
CaseTypeГруппировка кейсовname, cooldownHours, isFree
CaseRewardСвязь кейса с наградамиcaseId, rewardId, weight
UserFreeCaseOpensКупоны пользователяuserId, caseId, count
CaseOpeningИстория открытийuserId, caseId, rewardSnapshot, createdAt

Relationships


6. API Endpoints

МетодЭндпоинтОписаниеDocs
GET/api/casesСписок активных кейсов
GET/api/cases/:idДетали кейса
POST/api/cases/:id/openОткрытие (RNG)

  • Daily Spins — похожая механика ежедневных наград
  • Inventory — куда попадают выигранные предметы
  • Luck Pool — буст вероятностей для активных игроков
  • Budget — контроль выдачи ценных наград
  • Buffs — баффы удачи влияют на RNG
  • Streaks — Streak Cases открываются за Streak Points