Skip to main content

Promo Codes


1. Summary

Goal: Система промокодов для маркетинговых кампаний — позволяет создавать коды с различными типами наград и ограничениями.

User Value: Пользователь получает бонусы (валюту, опыт, предметы, купоны на кейсы) за активацию кодов от блогеров, партнёров или в рамках акций.


2. Business Logic

Reward Types

Валюта

Начисляет указанное количество Scrap на баланс пользователя.

Поля: rewardAmount — количество Scrap

Действие: user.scrap += rewardAmount, user.scrapTotalEarned += rewardAmount

Code Format

  • Только латиница и цифры: A-Z, 0-9
  • Case-insensitive — при вводе автоматически конвертируется в UPPERCASE
  • Хранится в БД в UPPERCASE
Integration Notes
  • Промокоды НЕ публикуются в Live Feed (FeedEvent)
  • При активации записывается в UserSeasonStats (сезонная статистика)

Redemption Flow

12 шагов валидации:

  1. Normalize — приводит код к UPPERCASE
  2. Find code — поиск в БД с relations (rewardItem, rewardCase)
  3. Check isActive — код не деактивирован админом
  4. Check startsAt — код уже активен (не в будущем)
  5. Check expiresAt — код не истёк
  6. Check maxRedemptions — лимит не исчерпан (totalRedemptions < maxRedemptions)
  7. Check alreadyRedeemed — пользователь не активировал ранее (по telegramId)
  8. Check onlyNewUsers — если установлен флаг: пользователь < 24h И без предыдущих redemptions
  9. Create snapshot — immutable JSON-копия награды
  10. Grant reward — применение награды к пользователю
  11. Find UTM — поиск последней UTM-сессии (7 дней) для атрибуции
  12. Record redemption — в транзакции: создание записи + инкремент счётчика
New User Definition

"Новый пользователь" для onlyNewUsers:

  • Аккаунт создан < 24 часов назад (NEW_USER_HOURS = 24)
  • И НЕТ ни одной предыдущей активации промокода

Оба условия должны выполняться одновременно.

Protection

ДействиеRate LimitAuthValidation
redeemgeneral (100/min)¹telegram + seasonredeemPromoCodeSchema
get historygeneraltelegram
admin CRUDmutationsadminvaries

¹ Rate limit планируется к добавлению

Season Restriction

Активация промокодов заблокирована между сезонами (статус COUNTDOWN). Middleware requireActiveSeason возвращает ошибку.

Security Details

См. Security Matrix для полного обзора защит.

No Steam Verification

Для активации промокодов НЕ требуется верификация Steam-аккаунта. Пользователь может использовать промокод сразу после регистрации.

Edge Cases

СитуацияUI поведениеКод
❌ Код не найден"Промокод не найден"NOT_FOUND
❌ Код деактивирован"Промокод деактивирован"INACTIVE
⏱️ Код ещё не активен"Промокод ещё не активен"NOT_STARTED
⏱️ Код истёк"Промокод истёк"EXPIRED
❌ Лимит исчерпан"Лимит использований исчерпан"EXHAUSTED
❌ Уже активировали"Вы уже активировали этот код"ALREADY_REDEEMED
❌ Только для новых"Только для новых пользователей"ONLY_NEW_USERS
✅ УспехПоказать награду
Константы и лимиты
КонстантаЗначениеОписание
NEW_USER_HOURS24Порог "нового пользователя" в часах
CODE_MIN_LENGTH (user)1Минимум символов для ввода
CODE_MIN_LENGTH (admin)3Минимум символов для создания
CODE_MAX_LENGTH50Максимальная длина кода
UTM_LOOKUP_DAYS7Окно для UTM атрибуции
LIST_DEFAULT_LIMIT20Пагинация по умолчанию
LIST_MAX_LIMIT100Максимум записей в списке
Алгоритм UTM Attribution

При активации промокода система ищет последний UTM-визит пользователя:

1. SELECT * FROM utm_tracking
WHERE userId = ?
AND createdAt > NOW() - 7 days
AND source IS NOT NULL
AND medium IS NOT NULL
AND campaign IS NOT NULL
ORDER BY createdAt DESC
LIMIT 1

2. SELECT id FROM utm_campaigns
WHERE source = ? AND medium = ? AND campaign = ?

Логика:

  • Поиск в utm_tracking по userId за последние 7 дней
  • Сопоставление с utm_campaigns через комбинацию source+medium+campaign
  • Если найдено — сохраняется utmCampaignId в PromoCodeRedemption

Файл: backend/src/domains/promo-codes/utils/utm-lookup.ts


3. ADR (Architectural Decisions)

ADR 1: Telegram ID как защита от мультиаккаунтов

Проблема: Пользователь может удалить аккаунт и зарегистрироваться заново, чтобы активировать промокод повторно.

Решение: Уникальный constraint (promoCodeId, telegramId) — проверка по Telegram ID, который переживает re-registration.

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

  • userId — не переживает удаление аккаунта
  • IP-адрес — меняется, VPN, ненадёжно

Последствия: Один промокод = один Telegram аккаунт навсегда. Даже при создании нового аккаунта повторная активация невозможна.

Data Consistency

Telegram ID хранится как String для совместимости с bigint значениями Telegram API.


ADR 2: Immutable Reward Snapshot

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

Решение: При активации сохраняется JSON snapshot награды (rewardSnapshot) — immutable копия того, что именно получил пользователь.

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

  • Ссылка на PromoCode — при изменении награды история "ломается"
  • Версионирование наград — избыточная сложность

Последствия: История всегда показывает что реально получил пользователь, независимо от последующих изменений конфигурации промокода.


ADR 3: HTTP 200 для бизнес-ошибок

Проблема: Как сообщать о бизнес-ошибках (код не найден, истёк, уже активирован)?

Решение: Возвращать 200 OK с { success: false, error: "CODE", errorMessage: "..." }.

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

  • HTTP 400/404 — не позволяет различить типы ошибок без парсинга body
  • Отдельные HTTP коды — нет стандартных кодов для "уже активирован"

Последствия: Консистентность с другими доменами (steam-verification). Фронтенд может легко обрабатывать различные типы ошибок по полю error.


4. Architecture

Services Overview

Key Components

КомпонентПутьОписание
PromoCodeServicebackend/src/domains/promo-codes/services/promo-code.service.tsUser-facing: redeem, getHistory
AdminPromoCodeServicebackend/src/domains/promo-codes/services/admin-promo-code.service.tsAdmin CRUD + stats
PromoCodeRepositorybackend/src/domains/promo-codes/repositories/promo-code.repository.tsData access layer
User Routesbackend/src/domains/promo-codes/routes/promo-code.routes.ts/api/promo-codes/*
Admin Routesbackend/src/domains/promo-codes/routes/admin-promo-code.routes.ts/admin/promo-codes/*
Schemasbackend/src/domains/promo-codes/schemas/promo-code.schemas.tsZod validation
UTM Lookupbackend/src/domains/promo-codes/utils/utm-lookup.tsCampaign attribution

5. Database Schema

Models

МодельОписаниеКлючевые поля
PromoCodeПромокодcode, rewardType, rewardAmount, maxRedemptions, onlyNewUsers, startsAt, expiresAt
PromoCodeRedemptionАктивацияtelegramId, rewardSnapshot, utmCampaignId, redeemedAt

Relationships

Indexes

ТаблицаIndexНазначение
PromoCodecodeБыстрый поиск при активации
PromoCodeisActiveФильтрация активных кодов
PromoCodeexpiresAtПроверка истечения
PromoCodeRedemption(promoCodeId, telegramId)UNIQUE — защита от повторной активации
PromoCodeRedemptionuserIdИстория пользователя
PromoCodeRedemptionutmCampaignIdАналитика кампаний

6. API Endpoints

МетодЭндпоинтОписаниеDocs
POST/api/promo-codes/redeemАктивировать промокод
GET/api/promo-codes/historyИстория активаций

POST /redeem

Request:

{
"code": "SUMMER2024"
}

Response (success):

{
"success": true,
"reward": {
"type": "SCRAP",
"amount": 500
}
}

Response (error):

{
"success": false,
"error": "EXPIRED",
"errorMessage": "Промокод истёк"
}

  • Cases — награда типа CASE даёт купон на бесплатное открытие
  • Inventory — награда типа ITEM добавляется с sourceType: PROMO_CODE
  • UTM Tracking — атрибуция промокодов к маркетинговым кампаниям
  • Seasons — активация заблокирована в статусе COUNTDOWN
  • Glossary — термины: Promo Code, Reward Snapshot, Coupon