- Lava Business via gate.lava.ru (HMAC-SHA256 signed JSON requests)
- Sub-methods: card and SBP via includeService filter
- Webhook signature verified from raw bytes with secret_key_2
- Sticky terminal-status guard (success after amount_mismatch escalates to ERROR)
- Order ID with full uuid4 hex (128-bit entropy)
- Cross-row contamination guard: order_id assertion on invoice_id fallback
- Warning when hook URL cannot be derived from webhook/web_api/cabinet bases
- Explicit failure when Lava response lacks payment_url (no orphan rows)
- Adds LAVA settings category, /lava-webhook endpoint, cabinet topup branch
- Mirrors existing Antilopay/Jupiter/Donut mixin pattern
- Fix webhook signature: str(None) produced "None" (4 chars) instead of
"" like PHP implode() does, causing all webhooks with custom_fields=null
to fail signature verification
- Add AURAPAY_SBP_ENABLED / AURAPAY_CARD_ENABLED env vars with display
names, following Freekassa pattern for sub-method selection
- Add aurapay_sbp / aurapay_card buttons in payment keyboard
- Add start_aurapay_sbp_topup / start_aurapay_card_topup handlers
- Route dispatch handles aurapay / aurapay_sbp / aurapay_card
- payment_utils updated with SBP/Card availability checks
- service parameter ("sbp"/"card") now passed through to AuraPay API
- balance/platega: re-set FSM state after min/max validation errors,
set state before pending_amount path, use balance_topup callback for back button
- balance/main: set FSM state and payment_method in handle_topup_amount_callback
for all providers before routing, use balance_topup callback in validation errors
- payment/paypear: fix confirmation_url key (was 'url'), add fallback,
store charged amount with commission for correct webhook amount comparison
- tariff_purchase: redirect to active tariff list when current tariff is
inactive (hidden trial after promo code activation)
- cabinet/renewal: check tariff.is_active in both GET and POST endpoints
to prevent hidden trial tariff periods from appearing
Show each Platega method (SBP, cards, crypto) as a separate button
on the main payment screen like YooKassa, instead of behind a sub-menu.
Controlled by PLATEGA_INLINE_METHODS env var (default: true).
Set to false to keep the old sub-menu behavior.
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
- Use API token as fallback for webhook signature verification per CryptoBot docs
- Try raw body, re-serialized compact JSON, and ASCII-escaped JSON for signature matching
- Auto-fill payment amount from saved cart in show_payment_methods instead of hardcoded 0
- API клиент (HMAC-SHA256 подпись, создание/получение платежа)
- CRUD операции с FOR UPDATE блокировкой
- Payment mixin с обработкой webhook и финализацией
- Хендлеры бота для пополнения через SeverPay
- Миграция 0040: таблица severpay_payments
- Webhook endpoint (всегда 200 для предотвращения ретраев)
- Интеграция с payment_verification_service
- Поддержка гостевых покупок (лендинги, подарки)
All min/max amount error messages in payment handlers now include
a back button keyboard, so users aren't stuck without navigation.
Fixed 30 message.answer() calls across 12 payment handler files.
- Remove estimated renewal price display from balance top-up screen
- Remove country name generation during server/squad sync, use original RemnaWave name as display_name
- Add html.escape() for all display_name/country name values rendered in HTML-parsed Telegram messages
- balance/main.py: single period discount on combined total (base + devices),
add promo-offer discount, fix device_limit fallback to tariff_device_limit
- pricing.py: same combined discount + promo-offer, proper device_limit
fallback matching reference (is not None check)
- admin/users.py: delegate to calculate_renewal_price() which handles both
tariff and classic modes correctly, removing classic-only calculate_subscription_price
- menu.py: use renewal_service.calculate_pricing() for both price check and
charge to ensure consistency, add try/except with user-facing error,
show actual charged amount in success message
In tariff mode, period_prices already includes servers and traffic costs.
But show_payment_methods() and get_subscription_cost() were using the classic
additive formula, adding server and traffic prices on top of the tariff price.
Example: 49₽ tariff + 150₽ server + 150₽ traffic = 349₽ shown, should be 49₽.
Now both functions detect tariff mode and only add extra device costs beyond
the tariff's device_limit. Classic mode formula unchanged.
- Quick amount buttons now calculate full renewal cost (base + devices + servers + traffic with discounts)
- Tariff mode uses tariff-specific device pricing (device_price_kopeks, device_limit)
- Broadcast inline buttons no longer crash with "no text in message to edit" on photo/video messages
- Media messages are now handled in _edit_with_photo: delete old message + send new text
- 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
- Remove misleading "Важно" and "При наличии корзины" warnings from all
payment success notifications
- Fix cart total bug: show actual cart price from Redis instead of top-up
amount, and suppress "insufficient funds" when balance is enough
- Extract shared send_cart_notification_after_topup() in common.py to
replace duplicated code across all 10 payment providers
Split Freekassa into sub-methods: СБП/QR (i=44) and Карты РФ (i=36).
Each method has independent enable/display_name settings, dedicated
handlers, keyboard buttons, and correct payment_system_id routing.
Webhook notifications resolve display name from payment metadata.
SUBSCRIPTION_PAYMENT transactions are stored with negative amount_kopeks.
- get_user_total_spent_kopeks now returns abs() to fix "Потрачено: -155 ₽"
and broken promo group threshold comparisons
- Balance history uses abs() before format_price to prevent "--85 ₽"
- 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
- 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
Добавлена возможность ограничивать пользователям:
- Пополнение баланса (restriction_topup)
- Покупку/продление подписки (restriction_subscription)
Изменения:
- models.py: добавлены поля restriction_topup, restriction_subscription,
restriction_reason и property has_restrictions
- universal_migration.py: миграция для новых полей
- admin/users.py: меню управления ограничениями в карточке пользователя
- keyboards/admin.py: клавиатура ограничений с toggle-кнопками
- states.py: состояние editing_user_restriction_reason
Проверки ограничений добавлены на двух уровнях:
- start_*_payment: при выборе метода оплаты
- process_*_payment_amount: при создании платежа
Затронутые провайдеры: stars, yookassa, mulenpay, wata, pal24,
cryptobot, heleket, platega, tribute, cloudpayments
При ограничении пользователь видит причину и кнопку "Обжаловать",
ведущую на контакт поддержки из настроек.