Three bugs caused trial subscriptions to be auto-renewed without a
tariff at arbitrary prices:
1. try_auto_extend_expired_after_topup: is_trial guard used truthiness
check — NULL (legacy rows) passed as falsy. Changed to
`is_trial is not False` (NULL-safe).
2. Multi-tariff branch: `not s.is_trial` treated NULL as not-trial.
Changed to `s.is_trial is False`.
3. Telegram bot autopay toggle: no is_trial guard — users could enable
autopay on trial subscriptions. Added trial check before enabling.
Two bugs caused "RemnaWave UUID не найден" when a user repurchased
after their panel user was deleted (expired user cleanup):
1. Webhook handler only cleared subscription.remnawave_uuid in
multi-tariff mode. In single-tariff mode the stale UUID remained,
causing the cabinet to try update_remnawave_user on a deleted
panel user instead of creating a new one.
2. Cabinet purchase-tariff used subscription.remnawave_uuid for the
create/update decision. In single-tariff mode this was stale.
Now mirrors the bot handler logic: checks user.remnawave_uuid
in single-tariff mode (correctly cleared by webhook).
The invite message wrapped the entire text including the referral URL
in <blockquote><code>...</code></blockquote>. The <code> tag made the
URL non-clickable — Telegram renders it as monospace copyable text.
Recipients couldn't tap the link to open it.
- Invite message: removed <code> from blockquote, Telegram now auto-links the URL
- Stats panel: removed <code> from bot/cabinet referral links, URLs are now clickable
timedelta.days is integer floor: 29 days 23 hours = 29, not 30.
When a user bought extra devices on the same day as their subscription,
they were charged for ~1 day instead of the full remaining period.
Fix: math.ceil(total_seconds / 86400) rounds partial days UP.
Applied to all 11 locations across 4 files:
- app/handlers/subscription/devices.py (5 spots)
- app/cabinet/routes/subscription_modules/devices.py (3 spots)
- app/keyboards/inline.py (3 spots — display pricing)
- app/utils/pricing_utils.py (1 spot — traffic prorated pricing)
The pricing engine applied promo group discounts unconditionally,
without checking if the tariff is available for the user's promo group.
In autopay: user with VIP group (60% discount, restricted to Premium
tariff) would get 60% off when auto-renewing a Basic tariff that their
group should not cover.
Fix: in _calculate_tariff_core, check tariff.is_available_for_promo_group
before applying group discounts. If tariff is not available for the
user's promo group, the discount is zeroed — subscription renews at
full price. Protects ALL pricing paths (autopay, recurrent, manual).
Add ADMIN_NOTIFICATIONS_{CATEGORY}_ENABLED settings (default True) for
all 10 notification categories: purchases, renewals, trials, balance,
addons, infrastructure, errors, promo, partners, tickets.
Setting ADMIN_NOTIFICATIONS_PROMO_ENABLED=false now completely suppresses
promo notifications (promocode activations, campaign visits, promo group
changes) instead of silently falling back to the general topic.
Also fix referral_contest_service direct bot.send_message bypass —
now respects ADMIN_NOTIFICATIONS_PROMO_ENABLED setting.
- Add get_subscription_request_history to RemnaWave API client
(GET /api/users/{uuid}/subscription-request-history with pagination)
- Add GET /admin/users/{user_id}/subscription-request-history endpoint
with subscription_id param for multi-tariff support
1. _check_expired_subscription_followups: added Subscription.status=EXPIRED
filter (was matching ALL statuses including ACTIVE), User.status=ACTIVE
filter, and 30-day lookback window to stop scanning ancient subscriptions
2. _get_expiring_paid_subscriptions: added User.status=ACTIVE filter to
prevent sending "expiring" notifications to blocked/deleted users
3. Multi-tariff: before sending expired/followup notifications, check if
user has another ACTIVE subscription with end_date > now — skip if they
still have service through another tariff
4. Multi-tariff: same check for _check_expired_subscriptions — don't send
"subscription expired" if user has another active sub
- Fix 7 intermediate error paths (balance deduction failures) that used
callback.answer() after the early answer was already consumed — user
got no error feedback at all
- Fix 2 unfixed handlers: confirm_tariff_purchase, confirm_daily_tariff_purchase
— same early-answer pattern applied
- All 7 purchase/extend/switch handlers now consistently use early
callback.answer() + edit_text for errors
Telegram invalidates callback queries after 30 seconds. When the bot
performed panel sync, DB transactions, and admin notifications before
answering, callback.answer() threw TelegramBadRequest: query is too old.
Moved callback.answer() to immediately after guard checks (balance,
tariff availability) in 5 handlers:
- confirm_tariff_extend
- confirm_custom_tariff_purchase
- confirm_tariff_switch
- confirm_daily_tariff_switch
- confirm_instant_switch
Error feedback now uses callback.message.edit_text() instead of the
expired callback.answer().
The "Unpin all" button called deactivate_active_pinned_message() first,
then looped over users to unpin. If Telegram API calls failed or timed
out, the message was already marked inactive in the DB with no way to
retry. Now: get active message → unpin from all chats → deactivate in DB.
Restore integration hooks dropped in PR #2851 merge:
- PurchaseRequest accepts yandex_cid, referrer, subid from frontend
- Cache yandex_cid and subid in Redis at purchase creation (24h TTL)
- On fulfill_purchase: extract subid from cache, persist to DB
- Save Yandex CID from Redis to yandex_client_id_map
- Fire on_registration + S2S postback for new accounts
- Fire on_purchase + S2S postback for all paid purchases
- All hooks wrapped in try/except — failures never block delivery
Two bugs caused max promo group assignment on gift send/activate:
1. Buyer: GIFT_PAYMENT was counted in get_user_total_spent_kopeks
alongside SUBSCRIPTION_PAYMENT. Now only SUBSCRIPTION_PAYMENT
counts as personal spending for promo group auto-assignment.
2. Recipient: fulfill_purchase and activate_purchase created a
SUBSCRIPTION_PAYMENT transaction for the recipient with the full
gift price. Now skipped for gift recipients — they didn't pay.
Replace str(exc) with generic 'Action failed: internal error' in both
_execute_for_user and _execute_for_subscription catch-all blocks.
Prevents leaking internal paths, SQL details, or connection strings.
- Thread admin_id through _execute_for_user to _do_delete_user for audit trail
- Replace raw exception str(e) with generic error message in client response
- Add subscriptions=[] to failure paths to prevent MissingGreenlet
- Add campaign_id and partner_id query params to GET /admin/users
- Filter users by advertising campaign via EXISTS subquery on registrations
- Filter users by partner via JOIN campaign registrations → campaigns
- Add DELETE_USER bulk action type with delete_from_panel param
- Handler calls UserService.delete_user_account for full bot+panel removal
- Permission check: users:delete required for delete_user action
- Add to _USER_LEVEL_ACTIONS (operates on user_ids, not subscription_ids)
- Add replaces_tab column to InfoPage ('faq','rules','privacy','offer')
- Migration 0067: add nullable replaces_tab column
- CRUD: clear_replaces_tab ensures one page per tab, get_tab_replacements
returns {tab: slug} mapping for active pages
- Admin routes: auto-clear old assignment on create/update
- Public route: GET /info-pages/tab-replacements (no auth)
- Schemas: replaces_tab with regex validation in all request/response models
- Add page_type column to InfoPage model ('page' or 'faq')
- Migration 0066: ALTER TABLE ADD COLUMN with server_default='page'
- Update schemas with page_type field and regex validation
- Update CRUD: page_type in create, filter in list
- Update admin/public routes with page_type query filter
- Backward compatible: existing pages default to type 'page'
- Remove triple-redundant slug index: keep only unique=True on column
(PostgreSQL creates unique index automatically), remove __table_args__
index and explicit create_index in migration
- Type ReorderRequest.items with ReorderItem(id: int, sort_order: int)
instead of raw dict — prevents unvalidated input causing 500
- Migration downgrade: just drop_table (unique constraint drops with it)
- InfoPage model: slug, title (JSONB locale dict), content (JSONB),
is_active, sort_order, icon, created_at/updated_at
- CRUD: create, get by id/slug, list, update, delete, reorder
- Admin routes: /admin/info-pages with full CRUD, toggle-active, reorder
(permissions: settings:read/settings:edit)
- Public routes: /info-pages list active, /info-pages/{slug} get by slug
- Migration 0065: create info_pages table with unique slug index
- Custom pages support: admins can create any info page with any slug
parse_mode='HTML' was missing from message sends during /start
registration. HTML tags like <a href="..."> were shown as literal
text instead of rendered links.
Fixed in 4 places:
- Privacy policy edit_text (line 1151)
- Privacy policy fallback answer (line 1158)
- Welcome/offer text in complete_registration_from_callback (line 1729)
- Welcome/offer text in complete_registration (line 2084)
tariff_id query param now accepts comma-separated IDs (e.g.
tariff_id=1,3,5). CRUD functions updated to use IN() operator
for multi-tariff server-side filtering. Pagination works correctly
with multiple tariffs selected.
Deactivates user in RemnaWave panel first, then deletes subscription
with related SubscriptionServer and TrafficPurchase records.
Subscription-level action (works with subscription_ids targeting).
- Add SET_DEVICES bulk action: sets device_limit on subscriptions,
syncs to RemnaWave panel (subscription-level action)
- Add device_limit to SubscriptionListItem and BulkSubscriptionInfo
schemas for frontend display
- Populate device_limit in _build_user_list_item and
_build_subscription_info helpers
subscriptions were only populated when is_multi_tariff_enabled()
was true. Users with multiple subscriptions in regular tariff mode
had empty subscriptions[] — bulk actions couldn't show or select them.
Now subscriptions are always populated regardless of tariff mode.