Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be00256618 | |||
| 7f693f2b58 | |||
| c3bf0dc0fd | |||
| d651a6c02f | |||
| d7039d75a4 | |||
| 6409b0c023 | |||
| af31c551d2 | |||
| a38dfcb75a | |||
| b7b83abb72 | |||
| f076269c32 | |||
| 8d16935c1c | |||
| 49d8de76a2 | |||
| b4d8cabbd8 | |||
| a7f3d652c5 | |||
| 38f3a9a16a | |||
| f7d33a7d2b | |||
| bd11801467 | |||
| e512e5fe6e | |||
| 799c83dd84 | |||
| 4cc18cbc9a | |||
| 4645be53cb | |||
| 79ea398d1d |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "3.16.0"
|
||||
".": "3.16.3"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
# 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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* auto-convert naive datetimes to UTC-aware on model load ([f7d33a7](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/f7d33a7d2b31145a839ee54676816aa657ac90da))
|
||||
* extend naive datetime guard to all model properties ([bd11801](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/bd11801467e917d76005d1a782c71f5ae4ffee6e))
|
||||
* handle naive datetime in raw SQL row comparison (payment/common) ([38f3a9a](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/38f3a9a16a24e85adf473f2150aad31574a87060))
|
||||
* handle naive datetimes in Subscription properties ([e512e5f](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/e512e5fe6e9009992b5bc8b9be7f53e0612f234a))
|
||||
* use AwareDateTime TypeDecorator for all datetime columns ([a7f3d65](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/a7f3d652c51ecd653900a530b7d38feaf603ecf1))
|
||||
|
||||
## [3.16.1](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.16.0...v3.16.1) (2026-02-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add migration for partner system tables and columns ([4645be5](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/4645be53cbb3799aa6b2b6a623af30460357a554))
|
||||
* add migration for partner system tables and columns ([79ea398](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/79ea398d1db436a7812a799bf01b2c1c3b1b73be))
|
||||
|
||||
## [3.16.0](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.15.1...v3.16.0) (2026-02-18)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
||||
|
||||
FROM python:3.13-slim
|
||||
|
||||
ARG VERSION="v3.16.0" # x-release-please-version
|
||||
ARG VERSION="v3.16.3" # x-release-please-version
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+227
-203
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
|
||||
@@ -61,7 +61,10 @@ class PaymentCommonMixin:
|
||||
)
|
||||
row = result.one_or_none()
|
||||
if row:
|
||||
is_active = row.status == 'active' and row.end_date > datetime.now(UTC)
|
||||
end_date = row.end_date
|
||||
if end_date is not None and end_date.tzinfo is None:
|
||||
end_date = end_date.replace(tzinfo=UTC)
|
||||
is_active = row.status == 'active' and end_date is not None and end_date > datetime.now(UTC)
|
||||
has_active_subscription = bool(is_active and not row.is_trial)
|
||||
except Exception as db_error:
|
||||
logger.warning(
|
||||
|
||||
+273
-235
@@ -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')
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""add partner system tables and columns
|
||||
|
||||
Revision ID: 0003
|
||||
Revises: 0002
|
||||
Create Date: 2026-02-18
|
||||
|
||||
Adds partner_status to users, creates withdrawal_requests and
|
||||
partner_applications tables, adds partner_user_id to advertising_campaigns,
|
||||
adds blocked_count to broadcast_history.
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '0003'
|
||||
down_revision: Union[str, None] = '0002'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
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 _has_table(table: str) -> bool:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
return table in inspector.get_table_names()
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1. users.partner_status
|
||||
if not _has_column('users', 'partner_status'):
|
||||
op.add_column('users', sa.Column('partner_status', sa.String(20), nullable=False, server_default='none'))
|
||||
op.create_index('ix_users_partner_status', 'users', ['partner_status'])
|
||||
|
||||
# 2. broadcast_history.blocked_count
|
||||
if _has_table('broadcast_history') and not _has_column('broadcast_history', 'blocked_count'):
|
||||
op.add_column('broadcast_history', sa.Column('blocked_count', sa.Integer(), nullable=True, server_default='0'))
|
||||
|
||||
# 3. advertising_campaigns.partner_user_id
|
||||
if _has_table('advertising_campaigns') and not _has_column('advertising_campaigns', 'partner_user_id'):
|
||||
op.add_column('advertising_campaigns', sa.Column('partner_user_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key(
|
||||
'fk_advertising_campaigns_partner_user_id',
|
||||
'advertising_campaigns',
|
||||
'users',
|
||||
['partner_user_id'],
|
||||
['id'],
|
||||
ondelete='SET NULL',
|
||||
)
|
||||
op.create_index('ix_advertising_campaigns_partner_user_id', 'advertising_campaigns', ['partner_user_id'])
|
||||
|
||||
# 4. withdrawal_requests table
|
||||
if not _has_table('withdrawal_requests'):
|
||||
op.create_table(
|
||||
'withdrawal_requests',
|
||||
sa.Column('id', sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id'), nullable=False, index=True),
|
||||
sa.Column('amount_kopeks', sa.Integer(), nullable=False),
|
||||
sa.Column('status', sa.String(50), nullable=False, server_default='pending', index=True),
|
||||
sa.Column('payment_details', sa.Text(), nullable=True),
|
||||
sa.Column('risk_score', sa.Integer(), server_default='0'),
|
||||
sa.Column('risk_analysis', sa.Text(), nullable=True),
|
||||
sa.Column('processed_by', sa.Integer(), sa.ForeignKey('users.id'), nullable=True),
|
||||
sa.Column('processed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('admin_comment', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# 5. partner_applications table
|
||||
if not _has_table('partner_applications'):
|
||||
op.create_table(
|
||||
'partner_applications',
|
||||
sa.Column('id', sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column(
|
||||
'user_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False
|
||||
),
|
||||
sa.Column('company_name', sa.String(255), nullable=True),
|
||||
sa.Column('website_url', sa.String(500), nullable=True),
|
||||
sa.Column('telegram_channel', sa.String(255), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('expected_monthly_referrals', sa.Integer(), nullable=True),
|
||||
sa.Column('status', sa.String(20), nullable=False, server_default='pending'),
|
||||
sa.Column('admin_comment', sa.Text(), nullable=True),
|
||||
sa.Column('approved_commission_percent', sa.Integer(), nullable=True),
|
||||
sa.Column(
|
||||
'processed_by', sa.Integer(), sa.ForeignKey('users.id', ondelete='SET NULL'), nullable=True
|
||||
),
|
||||
sa.Column('processed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table('partner_applications')
|
||||
op.drop_table('withdrawal_requests')
|
||||
op.drop_index('ix_advertising_campaigns_partner_user_id', table_name='advertising_campaigns')
|
||||
op.drop_constraint('fk_advertising_campaigns_partner_user_id', 'advertising_campaigns', type_='foreignkey')
|
||||
op.drop_column('advertising_campaigns', 'partner_user_id')
|
||||
op.drop_column('broadcast_history', 'blocked_count')
|
||||
op.drop_index('ix_users_partner_status', table_name='users')
|
||||
op.drop_column('users', 'partner_status')
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = 'remnawave-bedolaga-telegram-bot'
|
||||
version = "3.16.0"
|
||||
version = "3.16.3"
|
||||
description = 'Telegram bot for RemnaWave VPN service'
|
||||
readme = 'README.md'
|
||||
license = { text = 'MIT' }
|
||||
|
||||
Reference in New Issue
Block a user