Compare commits

...

10 Commits

Author SHA1 Message Date
Egor be00256618 Merge pull request #2631 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.16.3
2026-02-18 15:01:49 +03:00
github-actions[bot] 7f693f2b58 chore(main): release 3.16.3 2026-02-18 12:00:19 +00:00
Egor c3bf0dc0fd Merge pull request #2630 from BEDOLAGA-DEV/dev
Dev
2026-02-18 14:59:51 +03:00
Fringg d651a6c02f fix: eliminate deadlock by matching lock order with webhook
Deadlock: DELETE locks server_squads first, then subscriptions.
Webhook locks subscriptions first, then server_squads. Classic deadlock.

Fix: remove duplicate decrement block (was decrementing server_squads
twice), restructure subscription block to delete subscription FIRST
then decrement server_squads — matching webhook's lock acquisition order.
2026-02-18 12:24:08 +03:00
Fringg d7039d75a4 fix: connected_squads stores UUIDs, not int IDs — use get_server_ids_by_uuids
connected_squads JSON contains squad UUIDs like 'b4d782fa-...', not
integer IDs. int() cast fails on these. Now resolves UUIDs to integer
IDs via get_server_ids_by_uuids() before passing to remove_user_from_servers.
2026-02-18 12:17:27 +03:00
Fringg 6409b0c023 fix: auth middleware catches all commit errors, not just connection errors
When a handler swallows a DB error (e.g. ProgrammingError for missing
column), the transaction is aborted but the handler returns normally.
The auth middleware then tries db.commit() which fails with DBAPIError.

Now catches any exception on commit and does rollback, preventing the
cascade of "current transaction is aborted" errors through all
subsequent middleware layers.
2026-02-18 12:01:40 +03:00
Fringg af31c551d2 fix: 3 user deletion bugs — type cast, inner savepoint, lazy load
1. connected_squads JSON stores IDs as strings but server_squads.id is
   integer — cast to int before passing to remove_user_from_servers
2. Wrap remove_user_from_servers in its own db.begin_nested() so its
   failure doesn't abort the parent savepoint (subscription deletion)
3. Pre-fetch admin.id before delete_user_account to avoid MissingGreenlet
   when transaction rollback expires the ORM object
2026-02-18 11:59:25 +03:00
Fringg a38dfcb75a fix: wrap user deletion steps in savepoints to prevent transaction cascade abort
When one deletion step fails (e.g. missing campaign_id column in referral_earnings),
PostgreSQL aborts the entire transaction. All subsequent operations then fail with
"current transaction is aborted, commands ignored until end of transaction block".

Each of the 24 try/except blocks now uses `async with db.begin_nested():`
(PostgreSQL SAVEPOINT) so individual failures are isolated and rolled back
without poisoning the outer transaction.
2026-02-18 11:48:37 +03:00
Fringg b7b83abb72 fix: deadlock on user deletion + robust migration 0002
Decrement server_squads.current_users BEFORE deleting subscription
to match lock ordering with webhook handler, preventing deadlocks.

Also made migration 0002 robust with table existence checks to
prevent failures on DBs missing referral_earnings or
advertising_campaign_registrations tables.
2026-02-18 11:34:07 +03:00
Fringg f076269c32 fix: make migration 0002 robust with table existence checks
Migration was failing on DBs where referral_earnings or
advertising_campaign_registrations tables didn't exist yet,
causing campaign_id column to never be added. Added _has_table
and _has_column guards, wrapped backfill in existence check.
2026-02-18 11:30:38 +03:00
8 changed files with 348 additions and 270 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "3.16.2"
".": "3.16.3"
}
+13
View File
@@ -1,5 +1,18 @@
# Changelog
## [3.16.3](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.16.2...v3.16.3) (2026-02-18)
### Bug Fixes
* 3 user deletion bugs — type cast, inner savepoint, lazy load ([af31c55](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/af31c551d2f23ef01425bdb2db8f255dbc3047e2))
* auth middleware catches all commit errors, not just connection errors ([6409b0c](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/6409b0c023cd7957c43d5c1c3d83e671ccaf959c))
* connected_squads stores UUIDs, not int IDs — use get_server_ids_by_uuids ([d7039d7](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/d7039d75a47fbf67436a9d39f2cd9f65f2646544))
* deadlock on user deletion + robust migration 0002 ([b7b83ab](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/b7b83abb723913b3167e7462ff592a374c3f421b))
* eliminate deadlock by matching lock order with webhook ([d651a6c](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/d651a6c02f501b7a0ded570f2db6addcc16173a9))
* make migration 0002 robust with table existence checks ([f076269](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/f076269c323726c683a38db092d907591a26e647))
* wrap user deletion steps in savepoints to prevent transaction cascade abort ([a38dfcb](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/a38dfcb75a47a185d979a8202f637d8b79812e67))
## [3.16.2](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.16.1...v3.16.2) (2026-02-18)
+1 -1
View File
@@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
FROM python:3.13-slim
ARG VERSION="v3.16.2" # x-release-please-version
ARG VERSION="v3.16.3" # x-release-please-version
ARG BUILD_DATE
ARG VCS_REF
+5 -2
View File
@@ -1771,15 +1771,18 @@ async def full_delete_user(
panel_error: str | None = None
deleted_from_panel = False
# Pre-fetch admin.id to avoid MissingGreenlet after transaction rollback
admin_id_val = admin.id
# UserService.delete_user_account handles both bot DB and Remnawave panel
user_service = UserService()
success = await user_service.delete_user_account(db, user_id, admin.id)
success = await user_service.delete_user_account(db, user_id, admin_id_val)
if success:
deleted_from_panel = request.delete_from_panel and user.remnawave_uuid is not None
reason_text = f' (reason: {request.reason})' if request.reason else ''
logger.info('Admin fully deleted user', admin_id=admin.id, user_id=user_id, reason_text=reason_text)
logger.info('Admin fully deleted user', admin_id=admin_id_val, user_id=user_id, reason_text=reason_text)
return FullDeleteUserResponse(
success=success,
+7
View File
@@ -222,6 +222,13 @@ class AuthMiddleware(BaseMiddleware):
except (InterfaceError, OperationalError) as conn_err:
# Соединение закрылось (таймаут после долгой операции) - просто логируем
logger.warning('⚠️ Соединение с БД закрыто после обработки, пропускаем commit', conn_err=conn_err)
except Exception as commit_err:
# Transaction aborted (e.g. handler swallowed a ProgrammingError) — rollback
logger.warning('⚠️ Не удалось commit после обработки, rollback', commit_err=commit_err)
try:
await db.rollback()
except Exception:
pass
return result
except (InterfaceError, OperationalError) as conn_err:
+273 -235
View File
@@ -9,10 +9,7 @@ from sqlalchemy.orm import selectinload
from app.config import settings
from app.database.crud.promo_group import get_promo_group_by_id
from app.database.crud.subscription import (
decrement_subscription_server_counts,
get_subscription_by_user_id,
)
from app.database.crud.subscription import get_subscription_by_user_id
from app.database.crud.transaction import get_user_transactions_count
from app.database.crud.user import (
add_user_balance,
@@ -796,144 +793,141 @@ class UserService:
logger.error('❌ Ошибка деактивации RemnaWave как fallback', fallback_e=fallback_e)
try:
sent_notifications_result = await db.execute(
select(SentNotification).where(SentNotification.user_id == user_id)
)
sent_notifications = sent_notifications_result.scalars().all()
async with db.begin_nested():
sent_notifications_result = await db.execute(
select(SentNotification).where(SentNotification.user_id == user_id)
)
sent_notifications = sent_notifications_result.scalars().all()
if sent_notifications:
logger.info('🔄 Удаляем уведомлений', sent_notifications_count=len(sent_notifications))
await db.execute(delete(SentNotification).where(SentNotification.user_id == user_id))
await db.flush()
if sent_notifications:
logger.info('🔄 Удаляем уведомлений', sent_notifications_count=len(sent_notifications))
await db.execute(delete(SentNotification).where(SentNotification.user_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления уведомлений', error=e)
try:
if user.subscription:
subscription_servers_result = await db.execute(
select(SubscriptionServer).where(SubscriptionServer.subscription_id == user.subscription.id)
async with db.begin_nested():
user_messages_result = await db.execute(
update(UserMessage).where(UserMessage.created_by == user_id).values(created_by=None)
)
subscription_servers = subscription_servers_result.scalars().all()
await decrement_subscription_server_counts(
db,
user.subscription,
subscription_servers=subscription_servers,
)
if subscription_servers:
logger.info(
'🔄 Удаляем связей подписка-сервер', subscription_servers_count=len(subscription_servers)
)
await db.execute(
delete(SubscriptionServer).where(SubscriptionServer.subscription_id == user.subscription.id)
)
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления связей подписка-сервер', error=e)
try:
user_messages_result = await db.execute(
update(UserMessage).where(UserMessage.created_by == user_id).values(created_by=None)
)
if user_messages_result.rowcount > 0:
logger.info('🔄 Обновлено пользовательских сообщений', rowcount=user_messages_result.rowcount)
await db.flush()
if user_messages_result.rowcount > 0:
logger.info('🔄 Обновлено пользовательских сообщений', rowcount=user_messages_result.rowcount)
await db.flush()
except Exception as e:
logger.error('❌ Ошибка обновления пользовательских сообщений', error=e)
try:
promocodes_result = await db.execute(
update(PromoCode).where(PromoCode.created_by == user_id).values(created_by=None)
)
if promocodes_result.rowcount > 0:
logger.info('🔄 Обновлено промокодов', rowcount=promocodes_result.rowcount)
await db.flush()
async with db.begin_nested():
promocodes_result = await db.execute(
update(PromoCode).where(PromoCode.created_by == user_id).values(created_by=None)
)
if promocodes_result.rowcount > 0:
logger.info('🔄 Обновлено промокодов', rowcount=promocodes_result.rowcount)
await db.flush()
except Exception as e:
logger.error('❌ Ошибка обновления промокодов', error=e)
try:
welcome_texts_result = await db.execute(
update(WelcomeText).where(WelcomeText.created_by == user_id).values(created_by=None)
)
if welcome_texts_result.rowcount > 0:
logger.info('🔄 Обновлено приветственных текстов', rowcount=welcome_texts_result.rowcount)
await db.flush()
async with db.begin_nested():
welcome_texts_result = await db.execute(
update(WelcomeText).where(WelcomeText.created_by == user_id).values(created_by=None)
)
if welcome_texts_result.rowcount > 0:
logger.info('🔄 Обновлено приветственных текстов', rowcount=welcome_texts_result.rowcount)
await db.flush()
except Exception as e:
logger.error('❌ Ошибка обновления приветственных текстов', error=e)
try:
referrals_result = await db.execute(
update(User).where(User.referred_by_id == user_id).values(referred_by_id=None)
)
if referrals_result.rowcount > 0:
logger.info('🔗 Очищены реферальные ссылки у рефералов', rowcount=referrals_result.rowcount)
await db.flush()
async with db.begin_nested():
referrals_result = await db.execute(
update(User).where(User.referred_by_id == user_id).values(referred_by_id=None)
)
if referrals_result.rowcount > 0:
logger.info('🔗 Очищены реферальные ссылки у рефералов', rowcount=referrals_result.rowcount)
await db.flush()
except Exception as e:
logger.error('❌ Ошибка очистки реферальных ссылок', error=e)
try:
yookassa_result = await db.execute(select(YooKassaPayment).where(YooKassaPayment.user_id == user_id))
yookassa_payments = yookassa_result.scalars().all()
if yookassa_payments:
logger.info('🔄 Удаляем YooKassa платежей', yookassa_payments_count=len(yookassa_payments))
await db.execute(
update(YooKassaPayment).where(YooKassaPayment.user_id == user_id).values(transaction_id=None)
async with db.begin_nested():
yookassa_result = await db.execute(
select(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
)
await db.flush()
await db.execute(delete(YooKassaPayment).where(YooKassaPayment.user_id == user_id))
await db.flush()
yookassa_payments = yookassa_result.scalars().all()
if yookassa_payments:
logger.info('🔄 Удаляем YooKassa платежей', yookassa_payments_count=len(yookassa_payments))
await db.execute(
update(YooKassaPayment)
.where(YooKassaPayment.user_id == user_id)
.values(transaction_id=None)
)
await db.flush()
await db.execute(delete(YooKassaPayment).where(YooKassaPayment.user_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления YooKassa платежей', error=e)
try:
cryptobot_result = await db.execute(select(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id))
cryptobot_payments = cryptobot_result.scalars().all()
if cryptobot_payments:
logger.info('🔄 Удаляем CryptoBot платежей', cryptobot_payments_count=len(cryptobot_payments))
await db.execute(
update(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id).values(transaction_id=None)
async with db.begin_nested():
cryptobot_result = await db.execute(
select(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id)
)
await db.flush()
await db.execute(delete(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id))
await db.flush()
cryptobot_payments = cryptobot_result.scalars().all()
if cryptobot_payments:
logger.info('🔄 Удаляем CryptoBot платежей', cryptobot_payments_count=len(cryptobot_payments))
await db.execute(
update(CryptoBotPayment)
.where(CryptoBotPayment.user_id == user_id)
.values(transaction_id=None)
)
await db.flush()
await db.execute(delete(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления CryptoBot платежей', error=e)
try:
platega_result = await db.execute(select(PlategaPayment).where(PlategaPayment.user_id == user_id))
platega_payments = platega_result.scalars().all()
async with db.begin_nested():
platega_result = await db.execute(select(PlategaPayment).where(PlategaPayment.user_id == user_id))
platega_payments = platega_result.scalars().all()
if platega_payments:
logger.info('🔄 Удаляем Platega платежей', platega_payments_count=len(platega_payments))
await db.execute(
update(PlategaPayment).where(PlategaPayment.user_id == user_id).values(transaction_id=None)
)
await db.flush()
await db.execute(delete(PlategaPayment).where(PlategaPayment.user_id == user_id))
await db.flush()
if platega_payments:
logger.info('🔄 Удаляем Platega платежей', platega_payments_count=len(platega_payments))
await db.execute(
update(PlategaPayment).where(PlategaPayment.user_id == user_id).values(transaction_id=None)
)
await db.flush()
await db.execute(delete(PlategaPayment).where(PlategaPayment.user_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления Platega платежей', error=e)
try:
mulenpay_result = await db.execute(select(MulenPayPayment).where(MulenPayPayment.user_id == user_id))
mulenpay_payments = mulenpay_result.scalars().all()
async with db.begin_nested():
mulenpay_result = await db.execute(
select(MulenPayPayment).where(MulenPayPayment.user_id == user_id)
)
mulenpay_payments = mulenpay_result.scalars().all()
if mulenpay_payments:
mulenpay_name = settings.get_mulenpay_display_name()
logger.info(
'🔄 Удаляем платежей',
mulenpay_payments_count=len(mulenpay_payments),
mulenpay_name=mulenpay_name,
)
await db.execute(
update(MulenPayPayment).where(MulenPayPayment.user_id == user_id).values(transaction_id=None)
)
await db.flush()
await db.execute(delete(MulenPayPayment).where(MulenPayPayment.user_id == user_id))
await db.flush()
if mulenpay_payments:
mulenpay_name = settings.get_mulenpay_display_name()
logger.info(
'🔄 Удаляем платежей',
mulenpay_payments_count=len(mulenpay_payments),
mulenpay_name=mulenpay_name,
)
await db.execute(
update(MulenPayPayment)
.where(MulenPayPayment.user_id == user_id)
.values(transaction_id=None)
)
await db.flush()
await db.execute(delete(MulenPayPayment).where(MulenPayPayment.user_id == user_id))
await db.flush()
except Exception as e:
logger.error(
'❌ Ошибка удаления платежей',
@@ -942,206 +936,250 @@ class UserService:
)
try:
pal24_result = await db.execute(select(Pal24Payment).where(Pal24Payment.user_id == user_id))
pal24_payments = pal24_result.scalars().all()
async with db.begin_nested():
pal24_result = await db.execute(select(Pal24Payment).where(Pal24Payment.user_id == user_id))
pal24_payments = pal24_result.scalars().all()
if pal24_payments:
logger.info('🔄 Удаляем Pal24 платежей', pal24_payments_count=len(pal24_payments))
await db.execute(
update(Pal24Payment).where(Pal24Payment.user_id == user_id).values(transaction_id=None)
)
await db.flush()
await db.execute(delete(Pal24Payment).where(Pal24Payment.user_id == user_id))
await db.flush()
if pal24_payments:
logger.info('🔄 Удаляем Pal24 платежей', pal24_payments_count=len(pal24_payments))
await db.execute(
update(Pal24Payment).where(Pal24Payment.user_id == user_id).values(transaction_id=None)
)
await db.flush()
await db.execute(delete(Pal24Payment).where(Pal24Payment.user_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления Pal24 платежей', error=e)
try:
heleket_result = await db.execute(select(HeleketPayment).where(HeleketPayment.user_id == user_id))
heleket_payments = heleket_result.scalars().all()
async with db.begin_nested():
heleket_result = await db.execute(select(HeleketPayment).where(HeleketPayment.user_id == user_id))
heleket_payments = heleket_result.scalars().all()
if heleket_payments:
logger.info('🔄 Удаляем Heleket платежей', heleket_payments_count=len(heleket_payments))
await db.execute(
update(HeleketPayment).where(HeleketPayment.user_id == user_id).values(transaction_id=None)
)
await db.flush()
await db.execute(delete(HeleketPayment).where(HeleketPayment.user_id == user_id))
await db.flush()
if heleket_payments:
logger.info('🔄 Удаляем Heleket платежей', heleket_payments_count=len(heleket_payments))
await db.execute(
update(HeleketPayment).where(HeleketPayment.user_id == user_id).values(transaction_id=None)
)
await db.flush()
await db.execute(delete(HeleketPayment).where(HeleketPayment.user_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления Heleket платежей', error=e)
# Удаляем Freekassa платежи
try:
freekassa_payments_result = await db.execute(
select(FreekassaPayment).where(FreekassaPayment.user_id == user_id)
)
freekassa_payments = freekassa_payments_result.scalars().all()
if freekassa_payments:
logger.info('🔄 Удаляем Freekassa платежей', freekassa_payments_count=len(freekassa_payments))
await db.execute(
update(FreekassaPayment).where(FreekassaPayment.user_id == user_id).values(transaction_id=None)
async with db.begin_nested():
freekassa_payments_result = await db.execute(
select(FreekassaPayment).where(FreekassaPayment.user_id == user_id)
)
await db.flush()
await db.execute(delete(FreekassaPayment).where(FreekassaPayment.user_id == user_id))
await db.flush()
freekassa_payments = freekassa_payments_result.scalars().all()
if freekassa_payments:
logger.info('🔄 Удаляем Freekassa платежей', freekassa_payments_count=len(freekassa_payments))
await db.execute(
update(FreekassaPayment)
.where(FreekassaPayment.user_id == user_id)
.values(transaction_id=None)
)
await db.flush()
await db.execute(delete(FreekassaPayment).where(FreekassaPayment.user_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления Freekassa платежей', error=e)
# Удаляем Wata платежи (до транзакций, т.к. wata_payments.transaction_id -> transactions.id)
try:
wata_payments_result = await db.execute(select(WataPayment).where(WataPayment.user_id == user_id))
wata_payments = wata_payments_result.scalars().all()
async with db.begin_nested():
wata_payments_result = await db.execute(select(WataPayment).where(WataPayment.user_id == user_id))
wata_payments = wata_payments_result.scalars().all()
if wata_payments:
logger.info('🔄 Удаляем Wata платежей', wata_payments_count=len(wata_payments))
await db.execute(
update(WataPayment).where(WataPayment.user_id == user_id).values(transaction_id=None)
)
await db.flush()
await db.execute(delete(WataPayment).where(WataPayment.user_id == user_id))
await db.flush()
if wata_payments:
logger.info('🔄 Удаляем Wata платежей', wata_payments_count=len(wata_payments))
await db.execute(
update(WataPayment).where(WataPayment.user_id == user_id).values(transaction_id=None)
)
await db.flush()
await db.execute(delete(WataPayment).where(WataPayment.user_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления Wata платежей', error=e)
# Удаляем CloudPayments платежи
try:
cloudpayments_result = await db.execute(
select(CloudPaymentsPayment).where(CloudPaymentsPayment.user_id == user_id)
)
cloudpayments_payments = cloudpayments_result.scalars().all()
async with db.begin_nested():
cloudpayments_result = await db.execute(
select(CloudPaymentsPayment).where(CloudPaymentsPayment.user_id == user_id)
)
cloudpayments_payments = cloudpayments_result.scalars().all()
if cloudpayments_payments:
logger.info(
'🔄 Удаляем CloudPayments платежей', cloudpayments_payments_count=len(cloudpayments_payments)
)
await db.execute(
update(CloudPaymentsPayment)
.where(CloudPaymentsPayment.user_id == user_id)
.values(transaction_id=None)
)
await db.flush()
await db.execute(delete(CloudPaymentsPayment).where(CloudPaymentsPayment.user_id == user_id))
await db.flush()
if cloudpayments_payments:
logger.info(
'🔄 Удаляем CloudPayments платежей',
cloudpayments_payments_count=len(cloudpayments_payments),
)
await db.execute(
update(CloudPaymentsPayment)
.where(CloudPaymentsPayment.user_id == user_id)
.values(transaction_id=None)
)
await db.flush()
await db.execute(delete(CloudPaymentsPayment).where(CloudPaymentsPayment.user_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления CloudPayments платежей', error=e)
# Удаляем KassaAi платежи
try:
kassa_ai_result = await db.execute(select(KassaAiPayment).where(KassaAiPayment.user_id == user_id))
kassa_ai_payments = kassa_ai_result.scalars().all()
async with db.begin_nested():
kassa_ai_result = await db.execute(select(KassaAiPayment).where(KassaAiPayment.user_id == user_id))
kassa_ai_payments = kassa_ai_result.scalars().all()
if kassa_ai_payments:
logger.info('🔄 Удаляем KassaAi платежей', kassa_ai_payments_count=len(kassa_ai_payments))
await db.execute(
update(KassaAiPayment).where(KassaAiPayment.user_id == user_id).values(transaction_id=None)
)
await db.flush()
await db.execute(delete(KassaAiPayment).where(KassaAiPayment.user_id == user_id))
await db.flush()
if kassa_ai_payments:
logger.info('🔄 Удаляем KassaAi платежей', kassa_ai_payments_count=len(kassa_ai_payments))
await db.execute(
update(KassaAiPayment).where(KassaAiPayment.user_id == user_id).values(transaction_id=None)
)
await db.flush()
await db.execute(delete(KassaAiPayment).where(KassaAiPayment.user_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления KassaAi платежей', error=e)
try:
transactions_result = await db.execute(select(Transaction).where(Transaction.user_id == user_id))
transactions = transactions_result.scalars().all()
async with db.begin_nested():
transactions_result = await db.execute(select(Transaction).where(Transaction.user_id == user_id))
transactions = transactions_result.scalars().all()
if transactions:
logger.info('🔄 Удаляем транзакций', transactions_count=len(transactions))
await db.execute(delete(Transaction).where(Transaction.user_id == user_id))
await db.flush()
if transactions:
logger.info('🔄 Удаляем транзакций', transactions_count=len(transactions))
await db.execute(delete(Transaction).where(Transaction.user_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления транзакций', error=e)
try:
promocode_uses_result = await db.execute(select(PromoCodeUse).where(PromoCodeUse.user_id == user_id))
promocode_uses = promocode_uses_result.scalars().all()
async with db.begin_nested():
promocode_uses_result = await db.execute(
select(PromoCodeUse).where(PromoCodeUse.user_id == user_id)
)
promocode_uses = promocode_uses_result.scalars().all()
if promocode_uses:
logger.info('🔄 Удаляем использований промокодов', promocode_uses_count=len(promocode_uses))
await db.execute(delete(PromoCodeUse).where(PromoCodeUse.user_id == user_id))
await db.flush()
if promocode_uses:
logger.info('🔄 Удаляем использований промокодов', promocode_uses_count=len(promocode_uses))
await db.execute(delete(PromoCodeUse).where(PromoCodeUse.user_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления использований промокодов', error=e)
try:
referral_earnings_result = await db.execute(
select(ReferralEarning).where(ReferralEarning.user_id == user_id)
)
referral_earnings = referral_earnings_result.scalars().all()
async with db.begin_nested():
referral_earnings_result = await db.execute(
select(ReferralEarning).where(ReferralEarning.user_id == user_id)
)
referral_earnings = referral_earnings_result.scalars().all()
if referral_earnings:
logger.info('🔄 Удаляем реферальных доходов', referral_earnings_count=len(referral_earnings))
await db.execute(delete(ReferralEarning).where(ReferralEarning.user_id == user_id))
await db.flush()
if referral_earnings:
logger.info('🔄 Удаляем реферальных доходов', referral_earnings_count=len(referral_earnings))
await db.execute(delete(ReferralEarning).where(ReferralEarning.user_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления реферальных доходов', error=e)
try:
referral_records_result = await db.execute(
select(ReferralEarning).where(ReferralEarning.referral_id == user_id)
)
referral_records = referral_records_result.scalars().all()
async with db.begin_nested():
referral_records_result = await db.execute(
select(ReferralEarning).where(ReferralEarning.referral_id == user_id)
)
referral_records = referral_records_result.scalars().all()
if referral_records:
logger.info('🔄 Удаляем записей о рефералах', referral_records_count=len(referral_records))
await db.execute(delete(ReferralEarning).where(ReferralEarning.referral_id == user_id))
await db.flush()
if referral_records:
logger.info('🔄 Удаляем записей о рефералах', referral_records_count=len(referral_records))
await db.execute(delete(ReferralEarning).where(ReferralEarning.referral_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления записей о рефералах', error=e)
try:
conversions_result = await db.execute(
select(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
)
conversions = conversions_result.scalars().all()
async with db.begin_nested():
conversions_result = await db.execute(
select(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
)
conversions = conversions_result.scalars().all()
if conversions:
logger.info('🔄 Удаляем записей конверсий', conversions_count=len(conversions))
await db.execute(delete(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id))
await db.flush()
if conversions:
logger.info('🔄 Удаляем записей конверсий', conversions_count=len(conversions))
await db.execute(
delete(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
)
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления записей конверсий', error=e)
try:
broadcast_history_result = await db.execute(
select(BroadcastHistory).where(BroadcastHistory.admin_id == user_id)
)
broadcast_history = broadcast_history_result.scalars().all()
async with db.begin_nested():
broadcast_history_result = await db.execute(
select(BroadcastHistory).where(BroadcastHistory.admin_id == user_id)
)
broadcast_history = broadcast_history_result.scalars().all()
if broadcast_history:
logger.info('🔄 Удаляем записей истории рассылок', broadcast_history_count=len(broadcast_history))
await db.execute(delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id))
await db.flush()
if broadcast_history:
logger.info(
'🔄 Удаляем записей истории рассылок', broadcast_history_count=len(broadcast_history)
)
await db.execute(delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id))
await db.flush()
except Exception as e:
logger.error('❌ Ошибка удаления истории рассылок', error=e)
try:
campaigns_result = await db.execute(
select(AdvertisingCampaign).where(AdvertisingCampaign.created_by == user_id)
)
campaigns = campaigns_result.scalars().all()
if campaigns:
logger.info('🔄 Очищаем создателя у рекламных кампаний', campaigns_count=len(campaigns))
await db.execute(
update(AdvertisingCampaign)
.where(AdvertisingCampaign.created_by == user_id)
.values(created_by=None)
async with db.begin_nested():
campaigns_result = await db.execute(
select(AdvertisingCampaign).where(AdvertisingCampaign.created_by == user_id)
)
await db.flush()
campaigns = campaigns_result.scalars().all()
if campaigns:
logger.info('🔄 Очищаем создателя у рекламных кампаний', campaigns_count=len(campaigns))
await db.execute(
update(AdvertisingCampaign)
.where(AdvertisingCampaign.created_by == user_id)
.values(created_by=None)
)
await db.flush()
except Exception as e:
logger.error('❌ Ошибка обновления рекламных кампаний', error=e)
try:
if user.subscription:
logger.info('🔄 Удаляем подписку', subscription_id=user.subscription.id)
await db.execute(
delete(SubscriptionServer).where(SubscriptionServer.subscription_id == user.subscription.id)
)
await db.execute(delete(Subscription).where(Subscription.user_id == user_id))
await db.flush()
async with db.begin_nested():
if user.subscription:
logger.info('🔄 Удаляем подписку', subscription_id=user.subscription.id)
# Save squad info before deleting subscription
squad_ids = user.subscription.connected_squads
# Delete subscription_servers and subscription FIRST
# Lock order: subscriptions → server_squads (matches webhook order)
await db.execute(
delete(SubscriptionServer).where(SubscriptionServer.subscription_id == user.subscription.id)
)
await db.execute(delete(Subscription).where(Subscription.user_id == user_id))
await db.flush()
# Decrement server_squads.current_users AFTER subscription delete
# to match lock ordering with webhook and avoid deadlocks
if squad_ids:
try:
from app.database.crud.server_squad import (
get_server_ids_by_uuids,
remove_user_from_servers,
)
int_squad_ids = await get_server_ids_by_uuids(db, list(squad_ids))
if int_squad_ids:
await remove_user_from_servers(db, int_squad_ids)
except Exception as sq_err:
logger.warning('⚠️ Не удалось уменьшить счётчик серверов', error=sq_err)
except Exception as e:
logger.error('❌ Ошибка удаления подписки', error=e)
@@ -20,42 +20,59 @@ branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add column (idempotent check)
def _has_table(name: str) -> bool:
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = [c['name'] for c in inspector.get_columns('referral_earnings')]
return name in inspector.get_table_names()
if 'campaign_id' not in columns:
def _has_column(table: str, column: str) -> bool:
conn = op.get_bind()
inspector = sa.inspect(conn)
return column in [c['name'] for c in inspector.get_columns(table)]
def upgrade() -> None:
# Skip if referral_earnings table doesn't exist yet
# (fresh DBs create it via create_all in 0001 with campaign_id already present)
if not _has_table('referral_earnings'):
return
if not _has_column('referral_earnings', 'campaign_id'):
op.add_column('referral_earnings', sa.Column('campaign_id', sa.Integer(), nullable=True))
op.create_foreign_key(
'fk_referral_earnings_campaign_id',
'referral_earnings',
'advertising_campaigns',
['campaign_id'],
['id'],
ondelete='SET NULL',
)
# Only create FK if target table exists
if _has_table('advertising_campaigns'):
op.create_foreign_key(
'fk_referral_earnings_campaign_id',
'referral_earnings',
'advertising_campaigns',
['campaign_id'],
['id'],
ondelete='SET NULL',
)
op.create_index('ix_referral_earnings_campaign_id', 'referral_earnings', ['campaign_id'])
# Backfill existing data — pick earliest campaign registration per user
# (matches runtime logic in get_user_campaign_id: ORDER BY created_at ASC LIMIT 1)
op.execute(
"""
UPDATE referral_earnings re
SET campaign_id = sub.campaign_id
FROM (
SELECT DISTINCT ON (user_id) user_id, campaign_id
FROM advertising_campaign_registrations
ORDER BY user_id, created_at ASC
) sub
WHERE sub.user_id = re.referral_id
AND re.campaign_id IS NULL
"""
)
# Backfill existing data — only if source table exists
if _has_table('advertising_campaign_registrations') and _has_table('referral_earnings'):
op.execute(
sa.text("""
UPDATE referral_earnings re
SET campaign_id = sub.campaign_id
FROM (
SELECT DISTINCT ON (user_id) user_id, campaign_id
FROM advertising_campaign_registrations
ORDER BY user_id, created_at ASC
) sub
WHERE sub.user_id = re.referral_id
AND re.campaign_id IS NULL
""")
)
def downgrade() -> None:
op.drop_index('ix_referral_earnings_campaign_id', table_name='referral_earnings')
op.drop_constraint('fk_referral_earnings_campaign_id', 'referral_earnings', type_='foreignkey')
op.drop_column('referral_earnings', 'campaign_id')
if _has_table('referral_earnings') and _has_column('referral_earnings', 'campaign_id'):
op.drop_index('ix_referral_earnings_campaign_id', table_name='referral_earnings')
op.drop_constraint('fk_referral_earnings_campaign_id', 'referral_earnings', type_='foreignkey')
op.drop_column('referral_earnings', 'campaign_id')
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = 'remnawave-bedolaga-telegram-bot'
version = "3.16.2"
version = "3.16.3"
description = 'Telegram bot for RemnaWave VPN service'
readme = 'README.md'
license = { text = 'MIT' }