3.23.1 (2026-03-06)
Масштабный релиз безопасности и стабильности: устранены race conditions в платёжной системе, завершена миграция FK-ограничений, исправлены ошибки при мерже аккаунтов и работе с промокодами.
🔒 Платёжная система — race conditions и атомарность
4984f20 — Комплексный аудит и исправление всех 10 платёжных провайдеров.
Проблема: При одновременных webhook-вызовах баланс мог зачислиться дважды — 8 из 10 провайдеров не имели защиты от гонок.
Что сделано:
- SELECT FOR UPDATE блокировка строки платежа во всех 9 провайдерах (CryptoBot, Heleket, MulenPay, Pal24, Wata, Platega, CloudPayments, Freekassa, KassaAI). YooKassa уже имела свой паттерн
- Атомарный коммит:
create_transaction(commit=False)+ единыйdb.commit()— баланс и транзакция фиксируются в одной DB-операции. Раньшеcreate_transaction()коммитил сам, и при падении между коммитами транзакция создавалась, но баланс не обновлялся emit_transaction_side_effects()— события (event_emitter, promo-группы, реферальные конкурсы) вызываются после коммита- Все
link_*_payment_to_transactionиспользуютdb.flush()вместоdb.commit() verify_payment_amount()— утилита проверки суммы webhook vs ожидаемой, для защиты от манипуляций- Platega: блокировка перенесена перед чтением metadata, обновление полей инлайн (без CRUD-функции которая коммитит)
- CloudPayments:
int(round(amount * 100))вместоint(amount * 100)— устранение ошибок округления float→kopeks - Heleket добавлен в
SUPPORTED_AUTO_CHECK_METHODS - PII удалены из логов yookassa webhook (заголовки запроса, тело, IP-адреса)
UniqueConstraint(external_id, payment_method)на таблице transactions + Alembic миграция 0017 с дедупликацией- Cabinet:
PaymentService(bot=bot)— исправлена инициализация без бота в admin_payments и balance
📦 31 файл изменён, +460 / −117 строк
🗃️ Миграция FK-ограничений
34c82c3 — ON DELETE CASCADE/SET NULL на все FK к users.id.
Проблема: 27 FK-ограничений к users.id не имели ondelete — при удалении юзера или восстановлении бэкапа с сиротами constraints нарушались.
Что сделано (миграция 0016):
- Очистка сирот во всех 53 child-таблицах (
DELETEдля non-nullable,SET NULLдля nullable полей) - Пересоздание 27 FK с
ON DELETE CASCADE(дляuser_id) илиSET NULL(дляcreated_by,processed_by)
📦 2 файла, +166 / −27 строк
fe393d2 — Доработка миграции: добавлены 27 пропущенных constraints.
broadcast_history.admin_id: изменён с CASCADE на SET NULL (nullable поле, сохраняем аудит)- Добавлены 27 FK, которые были только очищены от сирот, но не пересозданы с
ondelete - Итого все 53 FK→users.id теперь корректно обработаны
📦 2 файла, +31 / −3 строки
🔀 Мерж аккаунтов
1c89bd8 — UniqueViolation при мерже аккаунтов с общим OAuth/Telegram/Email ID.
Проблема: SQLAlchemy не гарантирует порядок UPDATE при flush() — если primary обновлялся раньше secondary, unique constraint срабатывал до очистки старого значения.
Решение: Очистка secondary → flush() → установка primary. Гарантирует что старое значение удалено до присвоения нового.
📦 1 файл, +25 / −9 строк
00a7db2 — Дедупликация promocode_uses при мерже аккаунтов.
Проблема: После добавления UniqueConstraint(user_id, promocode_id), простое переназначение user_id при мерже падало с IntegrityError если оба юзера использовали один промокод.
Решение: Перед переназначением удаляются дубликаты — записи secondary, для которых у primary уже есть использование того же промокода.
📦 1 файл, +8 / −1 строка
🎟️ Промокоды
7fb839a — Конвертация триалов, race condition, savepoints.
Проблемы:
- Trial-подписки отказывались при активации промокода (~20 из 300 юзеров)
- Race condition при параллельном использовании промокода
commit()/rollback()вcreate_promocode_useломали DB-сессию при ошибках
Что сделано:
extend_subscription: добавлен переходTRIAL → ACTIVE— триальные подписки конвертируются в платныеUniqueConstraintнаPromoCodeUse(user_id, promocode_id)+ миграция 0015 с дедупликациейcreate_promocode_use:begin_nested()+flush()вместоcommit()/rollback()(без коррупции сессии)- Race condition:
create_promocode_useвызывается ДО_apply_promocode_effects(резервирование) - Atomic SQL
INCREMENTдляcurrent_uses(защита от lost-update) mark_user_as_had_paid_subscription: savepoint вместо commit/rollback- Удалён мёртвый код:
use_promocode(),trial_subscription_not_eligible
📦 9 файлов, +92 / −47 строк
👥 RBAC — системные роли
7a7fb71 — Дубликаты системных ролей при переименовании.
Проблема: Bootstrap искал роли по name — переименование через UI создавало дубликат при следующем запуске.
Решение: Поиск по (is_system, level) вместо name. Bootstrap только добавляет новые permissions, не перезатирая кастомизацию админа.
📦 1 файл, +31 / −8 строк
🏆 Реферальные конкурсы
6713b34 — Комплексное исправление системы конкурсов.
- Float precision:
int(round(amount * 100))вместоint(amount * 100)для рубли→копейки - Порядок callback-хендлеров: специфичные
startswithрегистрируются первыми - FSM state filter на callback для предотвращения случайных срабатываний
- Upsert в
add_contest_eventвместо дубликатов - SQL фильтр в
get_contests_for_events— все активные конкурсы, не только по дате - Нормализация end-of-day (23:59:59.999999) для границ конкурсных периодов
- Guard
is_completedвcreate_transaction
📦 6 файлов, +42 / −11 строк
🖥️ UI — Админ-панель
04562fd — Кнопка «Назад» в тарифах вела в настройки вместо админ-панели.
Проблема: Тарифы доступны напрямую из главного меню админки, но callback_data кнопки «Назад» указывал на admin_submenu_settings.
Решение: Заменён на admin_panel во всех 4 местах.
📦 1 файл, 4 строки изменены