Форк оригинального бессонничного promocode_bot (Python/Docker/PostgreSQL) на TypeScript + Cloudflare Workers + D1 + grammY
  • TypeScript 81.8%
  • JavaScript 11.1%
  • HTML 7.1%
Find a file
2026-06-07 19:21:31 +03:00
dashboard fix: destroy proportionChart at start of render to prevent stale canvas on auto-refresh 2026-06-07 15:54:09 +03:00
docs docs: remove duplicate line in README, fix AD-006 formatting 2026-06-07 19:21:31 +03:00
hardcoded fix: исправления из аудита — критичные баги, hardcoded-списки, Agreemod v1/v2 2026-05-24 18:37:34 +03:00
scripts refactor: use placeholders for analytics dataset names in wrangler.jsonc, inject from .vars 2026-06-07 14:52:10 +03:00
src docs: add TODO notes and ADR for broken-period AE workaround 2026-06-07 18:38:36 +03:00
tests fix: resolve userId/username from both AE fields for all periods 2026-06-07 18:29:07 +03:00
.dev.vars.example feat: separate Analytics Engine datasets per environment, add migration script 2026-06-07 14:48:10 +03:00
.gitignore feat: separate Analytics Engine datasets per environment, add migration script 2026-06-07 14:48:10 +03:00
.prod.vars.example feat: separate Analytics Engine datasets per environment, add migration script 2026-06-07 14:48:10 +03:00
.stage.vars.example feat: separate Analytics Engine datasets per environment, add migration script 2026-06-07 14:48:10 +03:00
AGENTS.md chore: code cleanup and docs update 2026-06-07 16:36:24 +03:00
app.config.ts refactor: remove metricsStartDate, add codesTotal from D1, fix dashboard total calculation 2026-06-07 15:27:00 +03:00
biome.json fix: revert users() to AE, add D1 index, fix dashboard 'за за' 2026-06-05 04:39:54 +03:00
package-lock.json chore: обновление wrangler до v4, assert → with в index.ts 2026-05-24 19:32:03 +03:00
package.json chore: worker-configuration.d.ts в .gitignore, gen:types, secrets.required, docs 2026-05-31 02:58:08 +03:00
README.md docs: remove duplicate line in README, fix AD-006 formatting 2026-06-07 19:21:31 +03:00
schema.sql fix: archive only OLD fixed codes (not pending) on confirm_type 2026-06-02 06:41:27 +03:00
tsconfig.json fix: 14 TypeScript-ошибок — typecheck проходит без ошибок 2026-05-25 04:55:40 +03:00
vitest.config.ts fix: synchronize docs and types with code — 10 inconsistencies fixed 2026-06-07 01:39:09 +03:00
wrangler.jsonc refactor: use placeholders for analytics dataset names in wrangler.jsonc, inject from .vars 2026-06-07 14:52:10 +03:00

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 как есть, без дополнительной подготовки.

Процесс загрузки

  1. Отправить файл боту → бот парсит и сохраняет во временную таблицу
  2. Показывает inline-клавиатуру с типами скидок и подсказкой (процент / фикс)
  3. Админ выбирает тип → промокоды переносятся в основную таблицу (fixed-типы архивируют старые неиспользованные коды)
  4. Или нажимает «Отмена» → временные данные удаляются

Все команды администратора блокируются, пока загрузка не завершена.

Бизнес-метрики

Проект собирает бизнес-метрики в 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

Просмотр метрик:

  1. В Telegram: /metrics (только админы)
  2. В дашборде: Cloudflare Pages (dashboard/) — docs/admin.md
  3. В 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.varsD1_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. Тесты по умолчанию пропущены.

Требования для запуска

  1. Фикстура tests/fixtures/test-users.json — скопировать и заполнить из test-users.example.json.
  2. Креды 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

Без кредов или фикстуры тесты пропускаются.