Replace pg_class lookup with information_schema.table_constraints query
that is schema-qualified and consistent with migration 0028 pattern.
Fixes constraint detection on fresh installs where create_all() creates
constraints that pg_class lookups could miss.
inspector.get_unique_constraints() fails to detect constraints created
by Base.metadata.create_all() on fresh installs, causing
DuplicateTableError. Query pg_class directly for reliable detection.
The column was left over from the old schema before prize_type/prize_value
refactoring. Its NOT NULL constraint caused INSERT failures since the
SQLAlchemy model no longer includes it.
Migration 0001 uses Base.metadata.create_all() which creates ALL tables
from current models.py, causing subsequent migrations (0015+) to fail
with "already exists" errors when they try to re-create constraints,
indexes, columns, and tables.
Three-layer fix:
1. migrations.py: detect fresh DB (no tables) and bootstrap via
create_all() + stamp head, bypassing all migrations entirely.
2. models.py: add EmailTemplate model, CheckConstraints to LandingPage,
and indexes to GuestPurchase so create_all() produces a complete
schema identical to running all 30 migrations sequentially.
3. Idempotency guards in migrations 0015-0030: _has_unique_constraint,
_has_table, _has_index, _has_column, _has_check_constraint checks
before DDL operations, protecting against re-runs via make migrate.
Add GET /admin/landings/{id}/stats endpoint returning:
- Summary stats (purchases, revenue, gifts, conversion rate)
- Daily breakdown for last 30 days (purchases, revenue, gifts per day)
- Tariff distribution (purchases and revenue per tariff)
Uses case() expressions for SQLite compatibility, timezone-safe
date grouping via func.timezone('UTC', ...), and composite index
(landing_id, status, paid_at) for query performance.
Add time-bounded percentage discounts with per-tariff overrides
and countdown timer support for landing pages.
- Add 5 discount columns to LandingPage model (percent, overrides,
starts_at, ends_at, badge_text) with Alembic migrations 0027-0028
- Add DB CHECK constraints for discount_percent range and date ordering
- Add discount price calculation in validate_and_calculate() and
public landing config endpoint with consistent formula
- Add admin CRUD with Pydantic validation, cascade-clear on removal,
merged date validation on partial updates, size bounds
- Remove discount_overrides from public API (baked into prices)
- Add size limits for allowed_tariff_ids and allowed_periods
- Add external_squad_uuid column to Tariff model with Alembic migration
- Add external_squad_uuid parameter to RemnaWave API create_user/update_user
- Pass external squad from tariff to RemnaWave on subscription creation/update
- Sync external squad in monitoring service, sync service, admin user management
- Clear external squad when tariff has none (consistent across all call sites)
- Add GET /available-external-squads endpoint with UUID validation and response model
- Update tariff schemas with UUID pattern validation
- Fix db.refresh to include tariff relationship for async safety
PostgreSQL не может автоматически привести строковый default к типу JSON.
Решение: убрать default, сменить тип, затем поставить новый default через raw SQL.
Мультиязычность:
- Миграция 0021: текстовые поля лендингов → JSON с ключами локалей
- Утилита resolve_locale_text с fallback-цепочкой (lang → ru → en → first)
- Админ API: dict[str, str] для title/subtitle/footer/meta/features
- Публичный API: ?lang= параметр, резолвит в плоские строки
- Обратная совместимость: plain strings → {"ru": value}
Гостевые платежи:
- Миграция 0022: user_id nullable во всех платёжных таблицах
- Поддержка всех провайдеров кроме Stars
- Общий хелпер try_fulfill_guest_purchase в common.py
- YooKassa переведена на общий хелпер
Исправления по ревью:
- CryptoBot: guest fulfillment после FOR UPDATE lock
- _patch_guest_metadata: commit вместо flush
- Freekassa/KassaAI: metadata как dict вместо JSON-строки
- purchase_token маскирован в логах
- None → "guest" в order_id
- Rate limit на GET /landing/{slug}
- Маскирование contact_value
- Модели LandingPage и GuestPurchase + миграции 0018/0019
- CRUD для лендингов и гостевых покупок
- Публичные роуты: GET /{slug}, POST /{slug}/purchase, GET /purchase/{token}
- Админ-роуты: CRUD лендингов с RBAC (manage_landings)
- Сервис guest_purchase_service: валидация, создание, фулфилмент
- Интеграция с PaymentService (YooKassa card/SBP) для гостевых платежей
- Webhook-обработка с идемпотентностью и атомарными транзакциями
- Rate limiting на публичных эндпоинтах
- YooKassaPayment.user_id теперь nullable для гостевых платежей
Вместо хардкода имён constraint'ов — ищем реальное имя через
pg_constraint. Пропускаем несуществующие таблицы и FK.
Исправляет краш при обновлении у пользователей с неполной схемой.
- SELECT FOR UPDATE блокировка во всех 9 провайдерах (кроме YooKassa — свой паттерн)
- create_transaction(commit=False) + единый db.commit() для атомарности
- emit_transaction_side_effects() для отложенных событий после коммита
- Все link_*_payment_to_transaction используют db.flush() вместо db.commit()
- Freekassa/KassaAI: прямое присвоение transaction_id + flush вместо update_status
- MulenPay: прямая мутация balance_kopeks вместо add_user_balance
- Platega: блокировка перед чтением metadata, инлайн обновления полей
- CloudPayments: int(round(amount * 100)) для корректного округления
- Heleket добавлен в SUPPORTED_AUTO_CHECK_METHODS
- Удалены PII из логов yookassa webhook (заголовки, IP)
- UniqueConstraint(external_id, payment_method) на транзакциях + миграция 0017
- Cabinet: PaymentService(bot=bot) внутри try блока
- verify_payment_amount утилита для проверки суммы webhook
- broadcast_history.admin_id: CASCADE→SET NULL (column is nullable, preserve audit trail)
- Added nullable=True to broadcast_history.admin_id in model
- Added 27 missing FK constraints to _FK_CHANGES (were only cleaned for orphans
but not recreated with ondelete)
- All 53 FK→users.id now consistently handled in both orphan cleanup and constraint recreation
27 FK ссылающихся на users.id не имели ondelete — при физическом удалении
юзера или восстановлении бэкапа с сиротами FK constraints не создавались.
Миграция 0016:
1. Чистит сироты во всех 53 child-таблицах (DELETE для non-nullable, SET NULL для nullable)
2. Пересоздаёт 27 FK с ON DELETE CASCADE (user_id) или SET NULL (created_by, processed_by)
- trial подписки теперь конвертируются в платные вместо отказа (ошибка ~20 из 300 юзеров)
- 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
- cleanup: удаление зарезервированной записи при ValueError от эффектов
- atomic SQL increment для current_uses (защита от lost-update)
- mark_user_as_had_paid_subscription: savepoint вместо commit/rollback
- удалён мёртвый код: use_promocode(), trial_subscription_not_eligible из маппингов
- Fix balance history display: referral_reward, refund, poll_reward now
shown as credits (💰 +amount) instead of expenses
- Fix double-counting: remove all Transaction-based REFERRAL_REWARD sum
queries from crud/referral.py, admin_stats.py, admin_users.py —
ReferralEarning is now the single source of truth
- Unify "active referrals" definition across cabinet, bot, and admin:
JOIN Subscription WHERE status=ACTIVE AND end_date > now()
- Add payment_method IS NOT NULL guard to get_user_own_deposits() to
exclude referral rewards historically mistyped as deposits
- Replace hardcoded transaction type strings with TransactionType enum
values in referral_withdrawal_service.py
- Add Alembic data migration (0014) to fix historical transactions:
UPDATE deposit → referral_reward WHERE payment_method IS NULL and
description matches referral patterns
Allow partners to specify their desired commission percentage (1-100%)
when applying. Field is optional and shown to admins during review.
Includes DB model, Alembic migration 0013, schema, route, and service changes.
Adds last_webhook_update_at, is_daily_paused, last_daily_charge_at,
remnawave_short_uuid to subscriptions table for databases where
these columns were not created by the initial schema migration.
- Fix critical bug: is_active_paid_subscription() guard was blocking
CHANNEL_REQUIRED_FOR_ALL from disabling paid subscriptions
- Add disable_trial_on_leave and disable_paid_on_leave columns to
RequiredChannel model with Alembic migration 0010
- Refactor enforcement logic in channel_member.py and channel_checker.py
to use per-channel settings instead of global env vars
- Update CRUD, Pydantic schemas, and admin API routes for new fields
- Add should_disable_subscription() and get_channel_settings() to
channel_subscription_service for per-channel decision logic
- Multi-channel subscription enforcement via middleware, events, and cabinet API
- 3-layer cache architecture: Redis -> PostgreSQL -> rate-limited Telegram API
- ChatMemberUpdated event-driven tracking with automatic VPN access control
- Admin management via bot FSM handler and REST API with full CRUD
- Channel ID normalization: @username resolved to numeric ID at creation time
- Fail-closed error handling: API errors deny access (security-first)
- Background reconciliation with keyset pagination (100 per batch)
- Per-user rate limiting on subscription check button (5s cooldown)
- Redis connection pooling via cache singleton (no per-request connections)
- Database: channel_id index, multi-row upsert optimization
- Localization: en, ru, zh, fa, ua translations for all new strings
- Frontend blocking UI with channel list and subscription status
- Admin channel management page with toggle, delete, and create
Old universal_migration.py created some tables (including email_templates)
with `timestamp` (naive) columns and had a catch-all that converted all
naive columns to `timestamptz` on each startup. After switching to Alembic,
that catch-all stopped running.
Users whose email_templates table was created by universal_migration.py
before the catch-all ran still have naive `timestamp` columns. The code
uses `datetime.now(UTC)` (timezone-aware), causing asyncpg to raise:
"can't subtract offset-naive and offset-aware datetimes"
Migration 0007 finds and converts ALL remaining naive timestamp columns
in public schema to timestamptz, assuming UTC for existing data.
Fixes: email template save returning 503 with DataError
- Add migration 0006 for blocked_count, channel, email_subject,
email_html_content columns missing from broadcast_history table
- Fix infinite trial reactivation loop in monitoring service
- Prevent webhook from overwriting freshly extended end_date
- Use tariff-specific pricing for auto-renewal instead of global config
- Add migration 0005 to re-apply missing columns from 0002-0004
(fixes DBs that were auto-stamped to head without running migrations)
- Add per-table error handling in backup ORM export so one table
failure doesn't break the entire backup
- Escape HTML in error notifications to prevent Telegram parse errors
Referral links from cabinet (?ref=CODE) were only tracked for email registration.
Now referral_code is accepted and processed in Telegram initData, Telegram Widget,
and OAuth authentication endpoints. Includes self-referral protection by email
for OAuth, proper error logging, and the missing email_templates table migration.
Migration was failing on DBs where referral_earnings or
advertising_campaign_registrations tables didn't exist yet,
causing campaign_id column to never be added. Added _has_table
and _has_column guards, wrapped backfill in existence check.
Existing databases stamped at 0001 (create_all checkfirst=True) are
missing new columns/tables from the partner system:
- users.partner_status
- broadcast_history.blocked_count
- advertising_campaigns.partner_user_id
- withdrawal_requests table
- partner_applications table
All checks are idempotent — safe for fresh and existing databases.
Adds nullable FK campaign_id to referral_earnings table, enabling
direct campaign ROI analytics without JOINing through registrations.
- Model: campaign_id column + AdvertisingCampaign relationship
- CRUD: get_user_campaign_id() helper, campaign_id param in create_referral_earning
- Service: resolve campaign_id in all earning creation paths
- Cabinet API: campaign_name in earnings response
- Migration 0002: add column + deterministic backfill via DISTINCT ON
Only apply alembic.ini logging config when root logger has no handlers
(CLI mode). When running programmatically, structlog is already configured
and fileConfig would replace its handlers, breaking all logging.
- Add OAuth provider config vars and helpers to config.py
- Add google_id, yandex_id, discord_id, vk_id columns to User model
- Create OAuth provider service with state management and 4 providers
- Add CRUD functions for OAuth user lookup, linking, and creation
- Add 3 API endpoints: providers list, authorize URL, callback
- Add alembic migration and universal_migration support
- Fix trial disable logic to cover OAuth auth_types
- Добавлен ENV переключатель TRAFFIC_TOPUP_ENABLED для вкл/выкл докупки
- Добавлена отдельная конфигурация пакетов TRAFFIC_TOPUP_PACKAGES_CONFIG
- Добавлено поле purchased_traffic_gb для отслеживания докупленного трафика
- Добавлены режимы расчета цены сброса (period/traffic/traffic_with_purchased)
- Исправлен абьюз: цена сброса теперь учитывает докупленный трафик
- Сброс purchased_traffic_gb при продлении/покупке подписки
- UX: меню сброса теперь показывает цену и баланс вместо alert
- UX: кнопка пополнения если не хватает средств на сброс
- Добавлена миграция для нового поля purchased_traffic_gb
- Добавлена локализация TRAFFIC_TOPUP_DISABLED (ru/en/ua/zh)
Изменения:
- Добавлены настройки модема в .env.example и config.py (MODEM_ENABLED, MODEM_PRICE_PER_MONTH, MODEM_PERIOD_DISCOUNTS)
- Добавлено поле modem_enabled в модель Subscription
- Реализован модуль handlers/subscription/modem.py с обработчиками подключения/отключения модема
- Добавлено управ