Add total_amount as an alias for cart_total in format() calls so custom
locale overrides using {total_amount} are properly substituted instead
of appearing as literal text in user messages.
event_object was referenced in _process_successful_yookassa_payment
but never passed to the method, causing all YooKassa webhook payments
to fail. Use payment.amount_kopeks from the database model instead.
- Add PENDING_ACTIVATION status for users with existing subscriptions
- Add activation endpoint POST /landing/activate/{token}
- Send email notifications on delivery and pending activation
- Add 3 email templates (delivered, activation required, gift received) in 5 languages
- Extract purchase status response builder to reusable helper
- Move activation logic to service layer
- Add header injection protection in email service
- Add Literal type guard for contact_type parameter
- Fix _mask_email crash on malformed input
- Pre-resolve notification params before commit to avoid DetachedInstanceError
Platega, Heleket, WATA, CloudPayments now accept optional return_url
and pass it to their APIs. Guest payments redirect back to cabinet
success page instead of Telegram bot.
- Remove `locked.is_paid = True` — is_paid is a read-only @property computed from status
- Change status from 'completed' to 'paid' (matching is_paid property check)
- Use db.commit() instead of db.flush() for guest payment persistence
Мультиязычность:
- Миграция 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 для гостевых платежей
- 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
Bug 1 improvement: Replaced double API call pattern (sync + update_remnawave_user)
with single _sync_subscription_to_panel call that accepts reset_traffic parameter.
This prevents TRIAL status being overwritten to EXPIRED by the second call's
different status computation logic.
Bug 2 improvement: Moved keyboard construction inside try block to prevent
AttributeError crash if locale keys are missing. Switched button text from
attribute access (texts.KEY) to defensive texts.get('KEY', fallback).
Added empty template guard to prevent sending empty messages to Telegram API.
Bug 1: Admin tariff change used update_remnawave_user() which returns
early when user has no remnawave_uuid. Restored _sync_subscription_to_panel()
which discovers/creates panel users via telegram_id/email fallback, then
applies traffic reset if RESET_TRAFFIC_ON_TARIFF_SWITCH is enabled.
Bug 2: Post-topup cart reminder in payment/common.py had hardcoded Russian
text sent to all users regardless of language. Replaced with localized
BALANCE_TOPPED_UP_CART_SUFFICIENT/INSUFFICIENT keys and used existing
MY_BALANCE_BUTTON/MAIN_MENU_BUTTON for inline keyboard buttons.
Added new i18n keys to all 5 locales (ru, en, ua, zh, fa).
- 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
- Replace test@example.com fallback with pool of 20 random emails
to avoid OP-SP-7 duplicate email errors from payment provider
- Fix metadata_json parsing: handle both dict (SQLAlchemy JSON column)
and string cases to prevent json.loads crash on dict input
- Add TypeError to exception handler for robustness
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.
- 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
- 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
- YooKassa: SELECT FOR UPDATE on payment row to prevent concurrent double-processing
- subtract_user_balance: row locking to prevent concurrent balance race conditions
- subtract_user_balance: transaction creation before commit for atomicity
- subscription renewal: compensating refund if extend_subscription fails after charge
- StaleDataError: use savepoint instead of full rollback to protect parent transaction
Cabinet was calling CryptoBotService.create_invoice() directly without
saving CryptoBotPayment to DB. When webhook arrived, payment lookup
failed and returned HTTP 400, causing infinite retries.
Now cabinet uses PaymentService.create_cryptobot_payment() (same as
miniapp) with proper USD conversion via currency_converter.
Also return HTTP 200 for unknown invoice_ids to stop retry spam.
Transaction created_at and completed_at showed identical timestamps
because webhook handlers created transactions with is_completed=True
in a single step. Now all 10 payment providers pass payment.created_at
to the transaction so created_at reflects when the user initiated
the payment, not when the webhook processed it.
Also: remove duplicate datetime import in inline.py, upgrade button
stats DB error logging from debug to warning, add index on
button_click_logs.button_type for analytics queries.
_process_heleket_payload deleted the invoice message on every call,
including manual "check status" presses. Now only deletes on final
statuses (paid, cancel, fail, etc.) so the payment UI stays visible
while the user is still waiting.
Also includes subscription fallback query fix (actual DB columns).
Subscription.is_active is a Python property, not a column — query
status/end_date/is_trial columns instead. Also restore subscription=None
initialization to avoid UnboundLocalError on line 112.
Rules editor crashed when preview truncated mid-HTML tag (e.g.
<blockquote> cut to <blockquo), causing Telegram parse error.
Strip HTML tags before truncating preview text.
Also fix MissingGreenlet in build_topup_success_keyboard: fall back
to a direct DB query instead of showing wrong button text.
Remove AUTO_ACTIVATE_AFTER_TOPUP and SHOW_ACTIVATION_PROMPT_AFTER_TOPUP
features from all payment providers, config, system settings, and tests.
Cart auto-purchase (AUTO_PURCHASE_AFTER_TOPUP) is preserved.
Bug fixes:
- fix KeyError 'months' in devices.py for custom locale overrides
- fix IntegrityError on trial subscription retry (update existing PENDING instead of INSERT)
- fix PendingRollbackError cascade by adding db.rollback() before recovery
- fix TelegramForbiddenError not caught in photo_message.py
- fix "query is too old" spam in required_sub_channel_check
- add missing trial locale keys (TRIAL_PAYMENT_DESCRIPTION, TRIAL_REFUND_DESCRIPTION, TRIAL_ACTIVATION_ERROR)
- Fix async context manager usage in sync_users: __aenter__() result
was not assigned, so hwid_api_client held the context manager object
instead of the actual API client, causing AttributeError on
reset_user_devices()
- Add user existence check in _restore_missing_yookassa_payment before
INSERT to prevent ForeignKeyViolationError when user_id from payment
metadata no longer exists in users table