Add 'buyer' to db.refresh attribute_names so the buyer relationship
is available when building the admin notification (prevents expired
attribute access in async context). Restructure recipient icon logic
to only compute when a recipient value exists.
Cabinet gift purchases now show "ПОДАРОК ИЗ КАБИНЕТА" instead of
"ПОКУПКА В ПОДАРОК С ЛЕНДИНГА". Buyer is resolved from the user
relationship with @username. Recipient shows "по коду активации"
when no direct recipient specified. Landing page slug line removed
for cabinet purchases.
Bot uses default HTML parse mode — all messages are HTML-parsed by Telegram.
Added html.escape() to all user-controlled and admin-controlled strings
before interpolation into HTML messages to prevent injection and parse errors.
49 files, ~250+ injection points fixed:
- user.full_name, first_name across all handlers and services
- tariff.name/description in purchase flow, admin panel, auto-purchase service
- campaign.name, start_parameter in admin and user-facing handlers
- group.name, promo_group.name across promo management
- contest.title, prize_text, leaderboard names (including public channels)
- transaction.description (contains raw user.full_name from referral service)
- restriction_reason across all balance and subscription handlers
- ticket.title, message_text, poll.title, poll.description
- welcome text template placeholders (first_name, username)
- maintenance reason, admin_name, selected_prize.display_name
New helpers in app/utils/formatting.py:
- safe_html_name() for escaping display names
- user_html_link() replacing 15+ duplicated inline link patterns
- Reorder middleware: LoggingMiddleware now outermost, GlobalErrorMiddleware inside — prevents full traceback logging for suppressed errors (message not modified, query too old, bot blocked)
- Fix root cause in message_patch.py: _edit_with_photo missing try/except for _original_edit_text when ENABLE_LOGO_MODE=False
- Add "message is not modified" suppression in AuthMiddleware to prevent unnecessary db.rollback() and ERROR-level logging
- Fix discount promo display in admin notifications: subscription_days shown as hours (not days), balance_bonus_kopeks shown as percentage (not price)
Добавлены 9 новых env-переменных для маршрутизации уведомлений по отдельным топикам:
- PURCHASES, RENEWALS, TRIALS, BALANCE, ADDONS
- INFRASTRUCTURE, ERRORS, PROMO, PARTNERS
Обратная совместимость: если топик для категории не задан — fallback на ADMIN_NOTIFICATIONS_TOPIC_ID.
1. Admin notification showed "renewal" instead of "first purchase" for new
users because has_had_paid_subscription was set before notification.
All 21 call sites now pass explicit purchase_type.
2. Partner referral not counted when mandatory channel subscription enabled.
required_sub_channel_check saved campaign_id but not referrer_id from
campaign.partner_user_id. Also removed duplicate DB query.
3. BOT_USERNAME auto-detection moved before web server start to close
race window on /cabinet/branding/telegram-widget endpoint.
Replace hardcoded raw HTML length checks (len(text) <= 900/1000/1024)
with centralized caption_exceeds_telegram_limit() that strips HTML tags
and unescapes entities before measuring against the real 1024-char limit.
Fixes logo disappearing when promo discounts add HTML markup to captions.
- Remove double html.escape on payment_method (already escaped in helper)
- Use attribute_names=['landing'] keyword arg in db.refresh for consistency
- Use async with Bot() for guaranteed session cleanup
- New send_guest_purchase_notification() method in AdminNotificationService
with blockquote for payment details, landing slug, buyer/recipient info
- Called from fulfill_purchase() for both DELIVERED and PENDING_ACTIVATION
- Called from activate_purchase() when pending purchase is activated
- Different titles: regular purchase, gift purchase, pending activation
- Properly typed GuestPurchase, html.escape on all user data
- Refresh purchase with ['landing'] relationship after commit
- html.escape fallback in _get_payment_method_display
- ruff formatting fixes
- send_subscription_purchase_notification: abs(transaction.amount_kopeks) when no explicit amount_kopeks passed
- send_subscription_renewal_notification: abs(transaction.amount_kopeks) for SubscriptionEvent storage
- Prevents negative amounts in admin Telegram messages and SubscriptionEvent records
- Campaign notifications: add tariff bonus display, hide empty promo group,
compact format matching purchase notification style
- Ticket notifications: send media (photos) in the same topic as the text
notification instead of separately. Uses caption for short texts, sequential
messages for long texts with correct message_thread_id routing
- fix N+1 query in money laundering analysis with GROUP BY batch query
- fix N+1 query in cabinet referral earnings with batch user fetch
- eliminate double balance stats computation in withdrawal flow
- replace in-memory referral counting with SQL COUNT/CASE aggregation
- fix HTML injection in admin Telegram notifications via html.escape()
- standardize return types for reject/complete withdrawal methods
SUBSCRIPTION_PAYMENT transactions have inconsistent signs in DB
(some negative, some positive). Add func.abs()/abs() to all SUM
queries and display code to ensure correct totals regardless of sign.
Affected: admin statistics, referral contest stats, tariff revenue,
campaign stats, reporting service, admin renewal notifications.
- Add ContextVarsMiddleware for automatic user_id/chat_id/username binding
via structlog contextvars (aiogram) and http_method/http_path (FastAPI)
- Use bound_contextvars() context manager instead of clear_contextvars()
to safely restore previous state instead of wiping all context
- Register ContextVarsMiddleware as outermost middleware (before GlobalError)
so all error logs include user context
- Replace structlog.get_logger() with structlog.get_logger(__name__) across
270 calls in 265 files for meaningful logger names
- Switch wrapper_class from BoundLogger to make_filtering_bound_logger()
for pre-processor level filtering (performance optimization)
- Migrate 1411 %-style positional arg logger calls to structlog kwargs
style across 161 files via AST script
- Migrate log_rotation_service.py from stdlib logging to structlog
- Add payment module prefixes to TelegramNotifierProcessor.IGNORED_LOGGER_PREFIXES
and ExcludePaymentFilter.PAYMENT_MODULES to prevent payment data leaking
to Telegram notifications and general log files
- Fix LoggingMiddleware: add from_user null-safety for channel posts,
switch time.time() to time.monotonic() for duration measurement
- Remove duplicate logger assignments in purchase.py, config.py,
inline.py, and admin/payments.py
Remove all modem purchase/management code:
- Delete modem handler, service, and tests
- Remove modem button from keyboards and admin panel
- Remove modem pricing from calculations
- Remove modem REST API endpoint and schemas
- Remove modem decorator, config settings, and notification formatting
- Keep DB column and migration for backwards compatibility
- Add html.escape() to all untrusted webhook data in admin and device
notifications (prevents HTML/Telegram injection)
- Add public send_webhook_notification() and is_enabled property to
AdminNotificationService (eliminates private method access)
- Add dedicated NotificationType enum values for device and not_connected
events (fixes incorrect semantic mapping)
- Extend user resolution to handle nested user objects and userUuid for
device-scope events
- Replace manual __anext__() DB session with AsyncSessionLocal context
manager; skip DB session for admin-only events
- Replace deprecated datetime.utcnow() with datetime.now(UTC)
- Use db.flush() instead of db.commit() in handlers (router commits)
- Wrap _notify_user in try/except to prevent notification failures from
rolling back successful DB mutations
- Add GitHub Markdown to Telegram HTML converter utility
- Place release description in blockquote expandable
- Auto-truncate description to fit 4096 char message limit
- Clean compact layout with clickable version link
- Convert markdown headers, bold, italic, code, links, strikethrough
- Add pyproject.toml with uv and ruff configuration
- Pin Python version to 3.13 via .python-version
- Add Makefile commands: lint, format, fix
- Apply ruff formatting to entire codebase
- Remove unused imports (base64 in yookassa/simple_subscription)
- Update .gitignore for new config files
1. app/keyboards/inline.py
- Добавлен параметр callback_data: str = "back_to_menu" в get_back_keyboard()
- Позволяет использовать кнопку "Назад" с разными callback'ами
2. app/services/admin_notification_service.py
- Добавлен тип "modem" в update_types с заголовком "📡 ИЗМЕНЕНИЕ МОД
При наличии докупленного трафика (например 250 + 10 ГБ = 260 ГБ)
система округляла текущий пакет до ближайшего (500 ГБ) и позволяла
бесплатно переключиться на него.
Исправления:
- confirm_switch_traffic: используется базовый трафик для расчёта цены
- get_traffic_switch_keyboard: добавлен параметр base_traffic_gb
- handle_switch_traffic: показывает информацию о докупленном трафике
- execute_switch_traffic: сбрасывает purchased_traffic_gb при переключении
2) У промогрупп появится приоритет
3) У пользователя может быть несколько промогрупп, но влиять будет только с наивысшим приоритетом
4) Промокоды с промогруппой
5) При выводе пользователей с промогруппой будет также выводиться ссылка на каждого. Можно будет отследить сливы промокодов "для своих". Я в целом это добавлю во все места, где пользователь выводится в админке
6) Исправить баг исчезновения триалки при пополнении
7) Исправить падающие тесты и добавить новых
8) Трафик: 0 ГБ в тестовой подписке исправить на Трафик: Безлимит
9) При попытке изменить промогруппу "Пользователь не найден" - исправил