- TypeScript 81.8%
- JavaScript 11.1%
- HTML 7.1%
| dashboard | ||
| docs | ||
| hardcoded | ||
| scripts | ||
| src | ||
| tests | ||
| .dev.vars.example | ||
| .gitignore | ||
| .prod.vars.example | ||
| .stage.vars.example | ||
| AGENTS.md | ||
| app.config.ts | ||
| biome.json | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| schema.sql | ||
| tsconfig.json | ||
| vitest.config.ts | ||
| wrangler.jsonc | ||
Promocode Bot (Cloudflare Workers edition)
Telegram-бот для выдачи промокодов (скидочных кодов) волонтёрам и организаторам фестиваля Insomnia.
Пользователь выбирает тип скидки через inline-кнопки, бот проверяет права доступа и лимиты, выдаёт свободный промокод.
История
Этот бот — форк и полная переработка оригинального promocode_bot (Python / Docker / PostgreSQL). Новая версия написана на TypeScript и развёрнута на Cloudflare Workers.
Принципиальные отличия от оригинала
| Оригинал | Версия CW | |
|---|---|---|
| Платформа | Python / Docker / VPS | TypeScript / CF Workers |
| База данных | PostgreSQL | D1 (SQLite) |
| Режим работы | Polling | Webhook |
| Аутентификация | Agreemod XOR hardcoded | Agreemod XOR hardcoded (useAgreemod, без автоматического fallback) |
| Роли в хардкоде | org, hr | org, hr, volunteer |
| Типы скидок | PERCENTAGE, FIRST_WAVE_PRICE, MINIMAL_PRICE | percent20, first_wave_price, minimal_price (из конфига) |
| Лимиты | Один общий (userlimits = 5) | Полностью из конфига (общий + на тип, для каждой роли) |
| Файлы промокодов | Только XLSX | CSV + XLSX |
| Ошибки в файле | Пропускаются молча | Сообщаются админу в ответе |
/stat |
Две секции одной таблицей | + топ-7 лидеров по каждому типу |
sql: |
Есть | Убрана |
| Тексты | Разбросаны по коду | texts.ts — единый файл |
| Ролевая модель | isOrg + isVolunteer (булевы) |
Единое поле role (org/hr/volunteer) |
| Метрики | Нет | Cloudflare Analytics Engine + SPA дашборд |
| Деплой | SSH + docker-compose | wrangler deploy |
Команды бота
| Команда | Доступ | Описание |
|---|---|---|
/start |
Все авторизованные | Приветствие |
/help |
Все авторизованные | Инструкция |
/claim |
org / hr / volunteer | Выбор типа скидки → выдача промокода |
/stat |
Админы | Статистика: детализация, сводка, топ-7 лидеров |
/metrics |
Админы | Бизнес-метрики: claims за 24ч, топ-5 лидеров |
/archive |
Админы | Архивировать все неиспользованные fixed-коды |
| Загрузка CSV/XLSX | Админы | Загрузка новых промокодов |
Подробнее о командах администратора — в docs/admin.md.
Роли и лимиты
| Роль | Источник аутентификации | Лимиты |
|---|---|---|
org (организатор) |
Agreemod participations (статус из config.orgStatuses) / hardcoded_org.txt |
Конфиг в app.config.ts |
hr (HR-волонтёр) |
Agreemod participations (статус из config.volunteerStatuses + UUID направления = HR) / hardcoded_hr.txt |
Конфиг в app.config.ts |
volunteer (волонтёр) |
Agreemod participations (статус из config.volunteerStatuses, без HR UUID) / hardcoded_volunteer.txt |
Конфиг в app.config.ts |
admin (админ) |
Указан в секрете ADMINS |
Всегда безлимит (не зависит от конфига) |
Приоритет при дублировании в hardcoded-списках: org > hr > volunteer.
Конфигурация доступа
Какие типы скидок доступны каждой роли и в каком количестве — определяется
в файле app.config.ts (секция limits). Тексты кнопок — в секции discountTypes[i].discountName.
// app.config.ts (фрагмент)
limits: {
org: { total: -1, percent20: -1, first_wave_price: -1, minimal_price: -1 },
hr: { total: -1, percent20: -1, first_wave_price: 0, minimal_price: 0 },
volunteer: { total: -1, percent20: -1, first_wave_price: 0, minimal_price: 0 },
},
-1— безлимит0— нет доступа (кнопка в/claimне показывается)- положительное число — лимит на количество промокодов этого типа
total — общий лимит на все типы. percent20, first_wave_price, minimal_price —
лимиты на конкретный тип. Проверка лимитов: сначала общий, потом на тип.
Файл app.config.ts коммитится в git и общий для всех окружений (staging / production).
При изменении лимитов нужно задеплоить бота.
Формат файла промокодов
Только для админов. Подробнее: docs/admin.md.
Файлы принимаются в двухколоночном формате ticketscloud: колонка A (code),
колонка B (amount). Колонка type не нужна — тип скидки выбирается через
inline-кнопки после загрузки.
CSV
Две колонки через запятую: code,amount. Первая строка может быть заголовком.
code,amount
SHFLSKJFH,10200
SKDJFHLKS,0.2
XLSX
Две колонки: code | amount. Можно использовать файлы ticketscloud как есть,
без дополнительной подготовки.
Процесс загрузки
- Отправить файл боту → бот парсит и сохраняет во временную таблицу
- Показывает inline-клавиатуру с типами скидок и подсказкой (процент / фикс)
- Админ выбирает тип → промокоды переносятся в основную таблицу (fixed-типы архивируют старые неиспользованные коды)
- Или нажимает «Отмена» → временные данные удаляются
Все команды администратора блокируются, пока загрузка не завершена.
Бизнес-метрики
Проект собирает бизнес-метрики в Cloudflare Analytics Engine (dataset promocode_bot_metrics):
| Тип события | Точки сбора |
|---|---|
claim |
success, limit_exceeded, no_codes |
auth |
success (agreemod/hardcoded/admin), denied, not_found, no_username |
file_upload |
success/fail + loaded count |
command |
любая команда + роль |
spam |
антиспам сработал |
timing |
время выполнения операции (xlsx_parse, db_batch, …) |
system_error |
необработанная ошибка grammY |
Просмотр метрик:
- В Telegram:
/metrics(только админы) - В дашборде: Cloudflare Pages (
dashboard/) — docs/admin.md - В Cloudflare Dashboard: Analytics → Analytics Engine →
promocode_bot_metrics
Быстрый старт
Требования
- Node.js 22+
- Wrangler CLI
- Аккаунт Cloudflare с включёнными Workers, D1 и Analytics Engine
- Telegram бот, созданный через @BotFather
1. Клонировать и установить зависимости
git clone <repo-url>
cd promocode_bot_cw
npm install
2. Создать базы данных D1
npx wrangler d1 create promocode-bot-db-staging
npx wrangler d1 create promocode-bot-db-production
Вставить полученные database_id в .stage.vars / .prod.vars → D1_DATABASE_ID.
Важно: binding должен быть строго
"DB"— именно так к нему обращается код (env.DB).
3. Настроить секреты
cp .stage.vars.example .stage.vars
vim .stage.vars # заполнить BOT_TOKEN, ADMINS
| Секрет | Обязательный? | Описание |
|---|---|---|
BOT_TOKEN |
✅ Да | Токен бота из @BotFather |
ADMINS |
✅ Да | Никнеймы админов через запятую |
D1_DATABASE_ID |
✅ Да (для деплоя) | ID базы D1 |
ANALYTICS_DATASET |
Нет | Имя датасета Analytics Engine (по умолчанию promocode_bot_metrics). Для staging/production задаётся в .vars |
WEBHOOK_SECRET |
Нет (рекомендуется) | Защита webhook (openssl rand -hex 32) |
AGREEMOD_URL |
Нет | URL Agreemod API (без него — runtime-ошибка при useAgreemod: true; не нужен при false) |
AGREEMOD_LOGIN |
Нет | Логин Agreemod |
AGREEMOD_PASSWORD |
Нет | Пароль Agreemod |
CF_ACCOUNT_ID |
Нет (без метрик) | Cloudflare Account ID |
CF_ANALYTICS_TOKEN |
Нет (без метрик) | API-токен с правом Analytics Read |
4. Создать hardcoded-списки
cp hardcoded/hardcoded_org.txt.template hardcoded/hardcoded_org.txt
cp hardcoded/hardcoded_hr.txt.template hardcoded/hardcoded_hr.txt
cp hardcoded/hardcoded_volunteer.txt.template hardcoded/hardcoded_volunteer.txt
Отредактировать .txt файлы — добавить никнеймы (по одному на строку, с @ или без).
5. Деплой
# staging (тестирование)
npm run deploy:staging
# Для production: npm run deploy:production
6. Установить webhook
# staging
curl "https://api.telegram.org/bot{STAGING_BOT_TOKEN}/setWebhook?url=https://promocode-bot-staging.{subdomain}.workers.dev&secret_token={STAGING_WEBHOOK_SECRET}"
# Для production замените URL и токен
Подробные инструкции
| Раздел | Документ |
|---|---|
| Разработка | docs/developer.md |
| Архитектура | docs/architecture.md |
| Развёртывание (бот + дашборд) | docs/deployment.md |
| Администрирование | docs/admin.md |
| ADR | docs/ARCHITECTURE_DECISIONS.md |
Интеграционные тесты с Agreemod API
Проект содержит интеграционные тесты (tests/integration/agreemod.test.ts),
которые проверяют контракт Agreemod API и полный pipeline authenticateUser.
Тесты по умолчанию пропущены.
Требования для запуска
- Фикстура
tests/fixtures/test-users.json— скопировать и заполнить изtest-users.example.json. - Креды Agreemod API — из
.stage.varsили напрямую.
Запуск
export $(grep -E '^(AGREEMOD_URL|AGREEMOD_LOGIN|AGREEMOD_PASSWORD)=' .stage.vars) && npm test
Или напрямую:
AGREEMOD_LOGIN=login AGREEMOD_PASSWORD=pass npm test
Три группы тестов:
- Структура ответа (7) — проверяют контракт ответа API (всегда pass)
- authenticateUser pipeline — для каждого пользователя из фикстуры тестируется полный цикл аутентификации
- participations — формат и роль (4) — проверяют year, direction_uuid, role и canary-тест на HR UUID
Без кредов или фикстуры тесты пропускаются.