Previously test squad was added to only one subscription. Now iterates
all active non-daily subscriptions and adds the test squad to each,
creating SubscriptionTemporaryAccess entries per subscription and
syncing each with RemnaWave.
Classic subscriptions (without tariff_id) now cannot be renewed or
auto-renewed when tariff mode is active. Users must purchase a tariff.
Blocked in: cabinet renewal endpoints, cabinet autopay, bot autopay
toggle, and auto-purchase service.
- Add tariff_id column to promocodes table (migration 0052)
- Admin can now select any tariff when creating trial_subscription promo
- Activation uses promocode.tariff_id if set, falls back to system
trial tariff
- Multi-tariff: blocks trial only if user already has that specific tariff
- Remove duplicate `from app.config import settings` inside function that
shadowed the module-level import, causing UnboundLocalError for all
SUBSCRIPTION_DAYS and TRIAL_SUBSCRIPTION promo types
- TRIAL_SUBSCRIPTION now resolves trial tariff via get_trial_tariff() /
TRIAL_TARIFF_ID and passes tariff_id, traffic_limit_gb, device_limit,
connected_squads to create_trial_subscription (was creating bare trial
without any tariff params)
- Multi-tariff: trial promo only blocks if user already has the specific
trial tariff, not any active subscription
- Cabinet endpoint now accepts subscription_id and returns
select_subscription response for multi-tariff SUBSCRIPTION_DAYS promos
When subscription.remnawave_uuid was None, the fallback to user.remnawave_uuid
caused the new subscription to overwrite the existing panel user instead of
creating a separate one.
In multi-tariff mode _resolve_admin_subscription was returning any active
subscription regardless of tariff_id, causing new tariff purchases to
overwrite the existing RemnaWave user instead of creating a new one.
- Replace disable_remnawave_user() with delete_remnawave_user() on subscription deletion
so the panel stops sending webhooks for deleted subscriptions
- Add early return in all webhook handlers when subscription is None (already deleted from DB):
expired, disabled, enabled, limited, traffic_reset, revoked, expiring reminders
- Add "Delete subscription" button in Telegram bot for expired/disabled subscriptions
with confirmation step and full cleanup (panel delete + server counts + DB hard delete)
When user clicks /start ref_CODE, the referral code was stored only in
FSM state. If the user opened miniapp/cabinet before completing bot
registration, the referral was lost.
Now /start immediately saves pending_referral:{telegram_id} to Redis
(7-day TTL). The referral is consumed by whichever path creates the
user first — bot create_user(), cabinet auth (initdata/widget/oidc).
Redis key is cleared after consumption to prevent double-referral.
- referral_service: save/get/clear_pending_referral Redis helpers
- start.py: save pending referral for new users only
- crud/user.py: create_user checks Redis if no referred_by_id
- cabinet/auth.py: initdata/widget/oidc routes check + cleanup Redis
- renewal.py: block renew/renewal-options for PENDING/DISABLED subscriptions
(extend_subscription doesn't transition these to ACTIVE — user would pay
for nothing)
- autopay.py: wrap 2x bare int() card_id parsing in try/except
- devices.py: wrap 2x bare int() device_count parsing in try/except
- daily_subscription_service: atomic daily charge — subtract_user_balance,
create_transaction, update_daily_charge_time all use commit=False, single
db.commit() after all three succeed. Prevents re-charge on partial failure.
- subscription.py: update_daily_charge_time accepts commit=False kwarg
When partial unique index (user_id + tariff_id) fires on concurrent or
duplicate tariff purchase via cabinet, the balance was already deducted
but no subscription was created. Now catches IntegrityError, rollbacks,
refunds via add_user_balance, and returns HTTP 409.
In multi-tariff mode, _handle_subscription_merge transfers ALL secondary
subscriptions to primary. Step 14 then iterated stale secondary.subscriptions
and nulled their remnawave_uuid, breaking the panel link for transferred subs.
Removed the UUID-nulling loop since all subs are already on primary.
HIGH fixes:
- auth.py: profile description sync now iterates all per-subscription
remnawave_uuids in multi-tariff mode
- admin_users: sync_from_panel uses subscription UUIDs for panel lookup,
does not overwrite user.remnawave_uuid in multi-tariff
MEDIUM fixes:
- monitoring_service: _send_subscription_expired_notification now takes
subscription param, uses se:{sub_id} in multi-tariff
- remnawave_webhook_service: _get_renew_keyboard accepts subscription_id,
all 7 callers pass it
- recurrent_payment_service: _build_extend_keyboard with subscription_id
- user_service: balance notification keyboards use menu_subscription in
multi-tariff instead of bare subscription_extend
- autopay.py + purchase.py: per-subscription cart deletion instead of
global delete_user_cart where subscription context available
- subscription_auto_purchase_service: 60-sec race guard changed from
per-user to per-subscription (checks subscription.updated_at)
- inline.py: open_subscription_link/subscription_connect callbacks now include
:{subscription_id} suffix in multi-tariff mode. Main menu uses subscription_connect
(picker) instead of bare open_subscription_link.
- guest_purchase_service: activate_purchase non-tariff path uses proper ordering
(non-daily, max days_left) instead of arbitrary _active[0]
- monitoring_service: _send_expired_day1_notification and discount notification
keyboards use se:{subscription.id} in multi-tariff (2 more hardcoded callbacks fixed)
- admin/tariffs: delete_tariff_confirmed now checks active subscription count
before deletion (RESTRICT FK). Prompt shows blocking message when active subs exist.
New CRUD function get_active_subscriptions_count_by_tariff_id.
- phantom_service: iterate all subscriptions for panel sync after claim
(was using deprecated user.subscription singular property)
- tariff_purchase: 6x delete_user_cart replaced with per-subscription
delete_subscription_cart in multi-tariff mode
- yookassa: recurrent payment subscription_id mismatch now resolves
correct subscription from metadata instead of just logging warning
- subscription_auto_purchase: try_auto_extend_expired and
try_resume_disabled_daily now query ALL subs (not just active) to find
expired/disabled subscriptions that need processing
- remnawave_service: sync_users_to_panel uses sub.remnawave_uuid in
multi-tariff instead of user.remnawave_uuid (was targeting wrong panel user)
- admin/users: admin_buy_subscription_execute saves UUID to
subscription.remnawave_uuid in multi-tariff (was saving to user)
- wheel_service: _process_days_payment and _apply_prize require subscription
in multi-tariff mode, fallback converts to balance bonus for prizes
Bot handlers (H1-H5):
- confirm_extend_subscription: error alert instead of wrong sub fallback
- open_subscription_link/subscription_connect: startswith registration
- handle_subscription_settings: multi-tariff guard
- confirm_reset_traffic: FSM state check in multi-tariff
Services (H6-H12):
- subscription_service: 5 UUID fallback fixes — no user.remnawave_uuid in
multi-tariff, return None if subscription.remnawave_uuid missing
- auto_purchase: use cart subscription_id for tariff match
- remnawave_service: migrate_squad_users checks subscription.remnawave_uuid
- campaign_service: extend existing sub or create new in multi-tariff
- broadcast_service: check ALL subs for paid-subscription guard
- blocked_users_service: remnawave_uuids list, iterate in cleanup
- user_service: log sub.remnawave_uuid in multi-tariff
Admin (H13-H16):
- grant_trial/paid_subscription: allow in multi-tariff mode
- promo_offers: pick sub with URL, aggregate squads from all subs
CRUD/Frontend (H17-H18):
- get_users_list: .unique() for outerjoin dedup
- refreshTraffic: withSubId in params instead of body
CRITICAL fixes:
- remnawave_service: panel_user.uuid AttributeError (3 places) — dict needs
.get('uuid'), not .uuid attribute access. Silent fail caused duplicate subs.
- remnawave_service: removed traffic_limit_gb, device_limit, connected_squads
overwrites from panel sync — bot is source of truth for these fields
- guest_purchase_service: multi-tariff now checks per-tariff (not any active
sub), allowing purchase of different tariffs simultaneously
- subscription_auto_purchase_service + user_cart_service: per-subscription cart
storage via user_cart:{user_id}:sub:{sub_id} keys. Cart resolution no longer
falls through to heuristic when saved_subscription_id lookup fails.
_delete_cart_for_subscription replaces delete_user_cart in all paths.
CRITICAL fixes:
- promocode_service: NameError (subscription_id not passed), TypeError (dict
returns), savepoint without commit, dead else branch
- cabinet status/autopay/renewal: resolve_subscription() instead of
user.subscription fallback in multi-tariff mode
- cabinet devices: MultipleResultsFound crash on 3 POST endpoints
- webhook service: IDOR returning cross-user subscription
- monitoring_service: real expiring notification keyboard with se:{sub_id}
HIGH fixes:
- subscription_purchase_service: FOR UPDATE on both branches of submit_purchase
- miniapp: 8 endpoints now pass subscription_id to _ensure_paid_subscription
- inline.py: se:{subscription_id} callback for expiring keyboard
- tariff_purchase: TransactionType.FAILED_REFUND + _persist_failed_refund()
- account_merge_service: panel sync after subscription transfer
- webhook service: .limit(1) on fallback queries to prevent MultipleResultsFound
servers.py + tariff_switch.py: use subscription.remnawave_uuid in multi-tariff
instead of user.remnawave_uuid to prevent duplicate panel users.
start.py: exception handler uses ['subscriptions'] (plural) for db.refresh.
my_subscriptions.py: delegation handlers pass state to downstream so
_resolve_subscription can read active_subscription_id from FSM.
admin_tariffs.py: removed trailing `or (sub.user.remnawave_uuid ...)`
that silently used wrong user-level UUID when subscription UUID was None.
tariff_purchase.py: IntegrityError handler in confirm_tariff_purchase now
restores consumed promo offer discount, matching the generic Exception handler.
When sync finds a panel user whose UUID matches User.remnawave_uuid
(legacy single-tariff) but not any Subscription.remnawave_uuid, it now
auto-links that UUID to the user's best active non-daily subscription.
This handles migration from single-tariff to multi-tariff mode without
losing panel user associations.
get_user_by_remnawave_uuid: fallback query searches Subscription table
when User-level UUID not found (multi-tariff stores UUID per-subscription).
Webhook _resolve_user_and_subscription: direct Subscription lookup before
returning None when user not found by telegram_id or User.remnawave_uuid.
Webhook user.deleted: refresh user.subscriptions before iterating to
ensure relationship is loaded from DB.
Account merge: clear subscription-level remnawave_uuid/short_uuid on
secondary user's subscriptions to prevent orphaned panel users.
select_tariff_extend_period and confirm_tariff_extend now use
_resolve_subscription instead of next() by tariff_id, preventing
wrong subscription selection with duplicate tariff_ids.
FSM state now stores active_subscription_id alongside extend_tariff_id.
confirm_daily_tariff_switch was searching for subscription matching the
TARGET tariff_id (the one being switched TO), which always returns None
since user doesn't have that tariff yet. Now uses _resolve_subscription
to get the source subscription (the one being switched FROM).
The MissingGreenlet fallback path in build_topup_success_keyboard
now queries all active/trial subscriptions and checks if ANY is active
paid, instead of only checking the most recently created one.
tariff_purchase.py:
- Switch lists filter ALL purchased tariffs, not just current one
- Switch handlers use _resolve_subscription (FSM state) instead of
searching by new tariff_id
- Extend shows subscription picker when >1 active subs
- All success screens return to sm:{sub_id} in multi-tariff
purchase.py:
- confirm_extend_subscription reads active_subscription_id from FSM state
common.py:
- get_reset_devices_confirm_keyboard accepts back_callback param
- get_confirm_switch_traffic_keyboard accepts back_callback param
traffic.py:
- confirm_switch_traffic passes dynamic back_callback
show_tariffs_list now fetches purchased_tariff_ids and passes them to
format_tariffs_list_text (marks with ✅) and get_tariffs_keyboard (marks button).
select_tariff blocks purchase of already-active tariff with alert showing
days remaining and directing user to "Мои подписки" for renewal.
When user clicks "Устройства" from subscription detail, shows intermediate
menu with two options:
- "Докупить устройства" (if tariff allows) → change device limit flow
- "Управление устройствами" → view/reset connected devices
Back button returns to subscription detail.
All keyboard builders (change_devices, confirm_change_devices,
devices_management, traffic_switch) now accept back_callback parameter.
In multi-tariff mode, back button returns to subscription detail (sm:{sub_id})
instead of legacy subscription_settings screen.
When user has >1 active subscription and clicks "Докупить трафик" or
"Подключиться" from main menu, now shows inline subscription picker
instead of just an alert. User selects subscription, then proceeds
to the corresponding flow. Removed redundant parse_mode (set globally).
Expiry, autopay success, daily charge, and traffic reset notifications
now append tariff name when multi-tariff is enabled, so users know which
subscription the notification is about.
All 4 remaining admin buttons (change server, devices limit, traffic limit,
reset devices) now include _s{subscription_id} suffix in callback_data.
Handlers extract subscription_id and operate on the correct subscription.
miniapp.py: get_subscription_details, get_tariffs, purchase_tariff,
preview_tariff_switch all resolve subscription from subscriptions list
instead of user.subscription property.
subscriptions.py + users.py: replace_existing and deactivation use
smart selection with len==1 guard.
channel_member: warns on UUID fallback in multi-tariff.
start.py: phantom merge checks subscription-level UUIDs before user-level transfer.
yookassa: validates subscription_id from recurrent payment metadata.
contest prize: notification includes tariff name for multi-subscription clarity.