diff --git a/app/external/telegram_stars.py b/app/external/telegram_stars.py
index 780b6ab7..8a0099cd 100644
--- a/app/external/telegram_stars.py
+++ b/app/external/telegram_stars.py
@@ -33,6 +33,7 @@ class TelegramStarsService:
try:
amount_rubles = amount_kopeks / 100
stars_amount = self.calculate_stars_from_rubles(amount_rubles)
+ stars_rate = settings.get_stars_rate()
invoice_link = await self.bot.create_invoice_link(
title=title,
@@ -46,7 +47,7 @@ class TelegramStarsService:
logger.info(
f"Создан Stars invoice на {stars_amount} звезд (~{int(amount_rubles)}₽) "
- f"для {chat_id}, курс: {int(settings.get_stars_rate())}₽/⭐"
+ f"для {chat_id}, курс: {stars_rate}₽/⭐"
)
return invoice_link
@@ -66,6 +67,7 @@ class TelegramStarsService:
try:
amount_rubles = amount_kopeks / 100
stars_amount = self.calculate_stars_from_rubles(amount_rubles)
+ stars_rate = settings.get_stars_rate()
message = await self.bot.send_invoice(
chat_id=chat_id,
@@ -80,7 +82,7 @@ class TelegramStarsService:
logger.info(
f"Отправлен Stars invoice {message.message_id} на {stars_amount} звезд "
- f"(~{int(amount_rubles)}₽), курс: {int(settings.get_stars_rate())}₽/⭐"
+ f"(~{int(amount_rubles)}₽), курс: {stars_rate}₽/⭐"
)
return {
"message_id": message.message_id,
diff --git a/app/handlers/balance.py b/app/handlers/balance.py
index 8be6b52a..bc9d6abd 100644
--- a/app/handlers/balance.py
+++ b/app/handlers/balance.py
@@ -149,19 +149,10 @@ async def show_payment_methods(
db_user: User,
state: FSMContext
):
- texts = get_texts(db_user.language)
+ from app.utils.payment_utils import get_payment_methods_text
- payment_text = """
-💳 Способы пополнения баланса
-
-Выберите удобный для вас способ оплаты:
-
-⭐ Telegram Stars - быстро и удобно
-💳 Банковская карта - через YooKassa/Tribute
-🛠️ Через поддержку - другие способы
-
-Выберите способ пополнения:
-"""
+ texts = get_texts(db_user.language)
+ payment_text = get_payment_methods_text()
await callback.message.edit_text(
payment_text,
@@ -171,6 +162,20 @@ async def show_payment_methods(
await callback.answer()
+@error_handler
+async def handle_payment_methods_unavailable(
+ callback: types.CallbackQuery,
+ db_user: User
+):
+ texts = get_texts(db_user.language)
+
+ await callback.answer(
+ "⚠️ В данный момент автоматические способы оплаты временно недоступны. "
+ "Для пополнения баланса обратитесь в техподдержку.",
+ show_alert=True
+ )
+
+
@error_handler
async def start_stars_payment(
callback: types.CallbackQuery,
@@ -360,7 +365,7 @@ async def process_stars_payment_amount(
texts = get_texts(db_user.language)
if not settings.TELEGRAM_STARS_ENABLED:
- await message.answer("⚠ Оплата Stars временно недоступна")
+ await message.answer("⚠️ Оплата Stars временно недоступна")
return
try:
@@ -368,6 +373,7 @@ async def process_stars_payment_amount(
amount_rubles = amount_kopeks / 100
stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles)
+ stars_rate = settings.get_stars_rate()
payment_service = PaymentService(message.bot)
invoice_link = await payment_service.create_stars_invoice(
@@ -385,7 +391,7 @@ async def process_stars_payment_amount(
f"⭐ Оплата через Telegram Stars\n\n"
f"💰 Сумма: {texts.format_price(amount_kopeks)}\n"
f"⭐ К оплате: {stars_amount} звезд\n"
- f"📊 Курс: {int(settings.get_stars_rate())}₽ за звезду\n\n"
+ f"📊 Курс: {stars_rate}₽ за звезду\n\n"
f"Нажмите кнопку ниже для оплаты:",
reply_markup=keyboard,
parse_mode="HTML"
@@ -395,7 +401,8 @@ async def process_stars_payment_amount(
except Exception as e:
logger.error(f"Ошибка создания Stars invoice: {e}")
- await message.answer("⚠ Ошибка создания платежа")
+ await message.answer("⚠️ Ошибка создания платежа")
+
@error_handler
@@ -790,3 +797,8 @@ def register_handlers(dp: Dispatcher):
check_cryptobot_payment_status,
F.data.startswith("check_cryptobot_")
)
+
+ dp.callback_query.register(
+ handle_payment_methods_unavailable,
+ F.data == "payment_methods_unavailable"
+ )
diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py
index 925edc33..9205c19a 100644
--- a/app/keyboards/inline.py
+++ b/app/keyboards/inline.py
@@ -458,7 +458,16 @@ def get_balance_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_payment_methods_keyboard(amount_kopeks: int, language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
-
+
+
+ if settings.TELEGRAM_STARS_ENABLED:
+ keyboard.append([
+ InlineKeyboardButton(
+ text="⭐ Telegram Stars",
+ callback_data="topup_stars"
+ )
+ ])
+
if settings.is_yookassa_enabled():
keyboard.append([
InlineKeyboardButton(
@@ -483,14 +492,6 @@ def get_payment_methods_keyboard(amount_kopeks: int, language: str = "ru") -> In
)
])
- if settings.TELEGRAM_STARS_ENABLED:
- keyboard.append([
- InlineKeyboardButton(
- text="⭐ Telegram Stars",
- callback_data="topup_stars"
- )
- ])
-
keyboard.append([
InlineKeyboardButton(
text="🛠️ Через поддержку",
@@ -498,6 +499,14 @@ def get_payment_methods_keyboard(amount_kopeks: int, language: str = "ru") -> In
)
])
+ if len(keyboard) == 1:
+ keyboard.insert(0, [
+ InlineKeyboardButton(
+ text="⚠️ Способы оплаты временно недоступны",
+ callback_data="payment_methods_unavailable"
+ )
+ ])
+
keyboard.append([
InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance")
])
diff --git a/app/services/backup_service.py b/app/services/backup_service.py
index a04fcf22..36782ec5 100644
--- a/app/services/backup_service.py
+++ b/app/services/backup_service.py
@@ -21,7 +21,7 @@ from app.database.models import (
ReferralEarning, Squad, ServiceRule, SystemSetting, MonitoringLog,
SubscriptionConversion, SentNotification, BroadcastHistory,
ServerSquad, SubscriptionServer, UserMessage, YooKassaPayment,
- CryptoBotPayment, Base
+ CryptoBotPayment, WelcomeText, Base
)
logger = logging.getLogger(__name__)
@@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
@dataclass
class BackupMetadata:
timestamp: str
- version: str = "1.0"
+ version: str = "1.1"
database_type: str = "postgresql"
backup_type: str = "full"
tables_count: int = 0
@@ -60,16 +60,32 @@ class BackupService:
self._auto_backup_task = None
self._settings = self._load_settings()
- self.backup_models = [
- User, Subscription, Transaction, PromoCode, PromoCodeUse,
- ReferralEarning, ServiceRule, SystemSetting,
- SubscriptionConversion, SentNotification, BroadcastHistory,
- ServerSquad, SubscriptionServer, UserMessage,
- YooKassaPayment, CryptoBotPayment
+ self.backup_models_ordered = [
+ ServiceRule,
+ SystemSetting,
+ Squad,
+ PromoCode,
+ ServerSquad,
+
+ User,
+
+ WelcomeText,
+ Subscription,
+ Transaction,
+ YooKassaPayment,
+ CryptoBotPayment,
+ PromoCodeUse,
+ ReferralEarning,
+ SubscriptionConversion,
+ BroadcastHistory,
+ UserMessage,
+
+ SentNotification,
+ SubscriptionServer,
]
if self._settings.include_logs:
- self.backup_models.append(MonitoringLog)
+ self.backup_models_ordered.append(MonitoringLog)
def _load_settings(self) -> BackupSettings:
return BackupSettings(
@@ -83,7 +99,6 @@ class BackupService:
)
def _parse_backup_time(self) -> Tuple[int, int]:
- """Возвращает часы и минуты для запланированного времени бекапа."""
time_str = (self._settings.backup_time or "").strip()
try:
@@ -137,12 +152,12 @@ class BackupService:
include_logs: bool = None
) -> Tuple[bool, str, Optional[str]]:
try:
- logger.info("🔄 Начинаем создание бекапа...")
+ logger.info("📄 Начинаем создание бекапа...")
if include_logs is None:
include_logs = self._settings.include_logs
- models_to_backup = self.backup_models.copy()
+ models_to_backup = self.backup_models_ordered.copy()
if not include_logs and MonitoringLog in models_to_backup:
models_to_backup.remove(MonitoringLog)
elif include_logs and MonitoringLog not in models_to_backup:
@@ -157,7 +172,16 @@ class BackupService:
table_name = model.__tablename__
logger.info(f"📊 Экспортируем таблицу: {table_name}")
- result = await db.execute(select(model))
+ query = select(model)
+
+ if model == User:
+ query = query.options(selectinload(User.subscription))
+ elif model == Subscription:
+ query = query.options(selectinload(Subscription.user))
+ elif model == Transaction:
+ query = query.options(selectinload(Transaction.user))
+
+ result = await db.execute(query)
records = result.scalars().all()
table_data = []
@@ -166,8 +190,12 @@ class BackupService:
for column in model.__table__.columns:
value = getattr(record, column.name)
- if isinstance(value, datetime):
+ if value is None:
+ record_dict[column.name] = None
+ elif isinstance(value, datetime):
record_dict[column.name] = value.isoformat()
+ elif isinstance(value, (list, dict)):
+ record_dict[column.name] = json_lib.dumps(value) if value else None
elif hasattr(value, '__dict__'):
record_dict[column.name] = str(value)
else:
@@ -266,7 +294,7 @@ class BackupService:
clear_existing: bool = False
) -> Tuple[bool, str]:
try:
- logger.info(f"🔄 Начинаем восстановление из {backup_file_path}")
+ logger.info(f"📄 Начинаем восстановление из {backup_file_path}")
backup_path = Path(backup_file_path)
if not backup_path.exists():
@@ -300,21 +328,25 @@ class BackupService:
logger.warning("🗑️ Очищаем существующие данные...")
await self._clear_database_tables(db)
- for table_name, records in backup_data.items():
+ models_by_table = {model.__tablename__: model for model in self.backup_models_ordered}
+
+ restore_order = []
+ for model in self.backup_models_ordered:
+ table_name = model.__tablename__
+ if table_name in backup_data and backup_data[table_name]:
+ restore_order.append(table_name)
+
+ for table_name in restore_order:
+ records = backup_data[table_name]
if not records:
continue
- model = None
- for m in self.backup_models:
- if m.__tablename__ == table_name:
- model = m
- break
-
+ model = models_by_table.get(table_name)
if not model:
logger.warning(f"⚠️ Модель для таблицы {table_name} не найдена, пропускаем")
continue
- logger.info(f"📥 Восстанавливаем таблицу {table_name} ({len(records)} записей)")
+ logger.info(f"🔥 Восстанавливаем таблицу {table_name} ({len(records)} записей)")
for record_data in records:
try:
@@ -326,9 +358,11 @@ class BackupService:
column = getattr(model.__table__.columns, key, None)
if column is None:
+ logger.warning(f"Колонка {key} не найдена в модели {table_name}")
continue
column_type_str = str(column.type).upper()
+
if ('DATETIME' in column_type_str or 'TIMESTAMP' in column_type_str) and isinstance(value, str):
try:
if 'T' in value:
@@ -340,7 +374,7 @@ class BackupService:
processed_data[key] = datetime.utcnow()
elif ('BOOLEAN' in column_type_str or 'BOOL' in column_type_str) and isinstance(value, str):
processed_data[key] = value.lower() in ('true', '1', 'yes', 'on')
- elif ('INTEGER' in column_type_str or 'INT' in column_type_str) and isinstance(value, str):
+ elif ('INTEGER' in column_type_str or 'INT' in column_type_str or 'BIGINT' in column_type_str) and isinstance(value, str):
try:
processed_data[key] = int(value)
except ValueError:
@@ -350,15 +384,19 @@ class BackupService:
processed_data[key] = float(value)
except ValueError:
processed_data[key] = 0.0
- elif 'JSON' in column_type_str and isinstance(value, str):
- try:
- processed_data[key] = json_lib.loads(value)
- except (ValueError, TypeError):
+ elif 'JSON' in column_type_str:
+ if isinstance(value, str) and value.strip():
+ try:
+ processed_data[key] = json_lib.loads(value)
+ except (ValueError, TypeError):
+ processed_data[key] = value
+ elif isinstance(value, (list, dict)):
processed_data[key] = value
+ else:
+ processed_data[key] = None
else:
processed_data[key] = value
- # Проверяем существует ли запись с таким ID
primary_key_col = None
for col in model.__table__.columns:
if col.primary_key:
@@ -366,7 +404,6 @@ class BackupService:
break
if primary_key_col and primary_key_col in processed_data:
- # Проверяем существование записи
existing_record = await db.execute(
select(model).where(
getattr(model, primary_key_col) == processed_data[primary_key_col]
@@ -374,18 +411,15 @@ class BackupService:
)
existing = existing_record.scalar_one_or_none()
- if existing:
- # Обновляем существующую запись
+ if existing and not clear_existing:
for key, value in processed_data.items():
- if key != primary_key_col: # Не обновляем primary key
+ if key != primary_key_col:
setattr(existing, key, value)
logger.debug(f"Обновлена существующая запись {primary_key_col}={processed_data[primary_key_col]} в {table_name}")
else:
- # Создаем новую запись
instance = model(**processed_data)
db.add(instance)
else:
- # Если нет primary key или он не в данных, просто добавляем
instance = model(**processed_data)
db.add(instance)
@@ -393,6 +427,7 @@ class BackupService:
except Exception as e:
logger.error(f"Ошибка восстановления записи в {table_name}: {e}")
+ logger.error(f"Проблемные данные: {record_data}")
continue
restored_tables += 1
@@ -432,11 +467,12 @@ class BackupService:
async def _clear_database_tables(self, db: AsyncSession):
tables_order = [
- "subscription_servers", "sent_notifications", "broadcast_history",
- "subscription_conversions", "referral_earnings", "promocode_uses",
- "transactions", "yookassa_payments", "cryptobot_payments",
- "subscriptions", "users", "promocodes", "server_squads",
- "service_rules", "system_settings", "monitoring_logs", "user_messages"
+ "subscription_servers", "sent_notifications",
+ "user_messages", "broadcast_history", "subscription_conversions",
+ "referral_earnings", "promocode_uses", "transactions",
+ "yookassa_payments", "cryptobot_payments", "welcome_texts",
+ "subscriptions", "users", "promocodes", "server_squads",
+ "squads", "service_rules", "system_settings", "monitoring_logs"
]
for table_name in tables_order:
@@ -472,7 +508,8 @@ class BackupService:
"file_size_bytes": file_stats.st_size,
"file_size_mb": round(file_stats.st_size / 1024 / 1024, 2),
"created_by": metadata.get("created_by"),
- "database_type": metadata.get("database_type", "unknown")
+ "database_type": metadata.get("database_type", "unknown"),
+ "version": metadata.get("version", "1.0")
}
backups.append(backup_info)
@@ -491,6 +528,7 @@ class BackupService:
"file_size_mb": round(file_stats.st_size / 1024 / 1024, 2),
"created_by": None,
"database_type": "unknown",
+ "version": "unknown",
"error": f"Ошибка чтения: {str(e)}"
})
@@ -563,7 +601,7 @@ class BackupService:
interval = self._get_backup_interval()
self._auto_backup_task = asyncio.create_task(self._auto_backup_loop(next_run))
logger.info(
- "🔄 Автобекапы включены, интервал: %.2fч, ближайший запуск: %s",
+ "📄 Автобекапы включены, интервал: %.2fч, ближайший запуск: %s",
interval.total_seconds() / 3600,
next_run.strftime("%d.%m.%Y %H:%M:%S")
)
@@ -571,7 +609,7 @@ class BackupService:
async def stop_auto_backup(self):
if self._auto_backup_task and not self._auto_backup_task.done():
self._auto_backup_task.cancel()
- logger.info("⏹️ Автобекапы остановлены")
+ logger.info("ℹ️ Автобекапы остановлены")
async def _auto_backup_loop(self, next_run: Optional[datetime] = None):
next_run = next_run or self._calculate_next_backup_datetime()
@@ -595,7 +633,7 @@ class BackupService:
next_run.strftime("%d.%m.%Y %H:%M:%S")
)
- logger.info("🔄 Запуск автоматического бекапа...")
+ logger.info("📄 Запуск автоматического бекапа...")
success, message, _ = await self.create_backup()
if success:
@@ -624,7 +662,7 @@ class BackupService:
icons = {
"success": "✅",
"error": "❌",
- "restore_success": "📥",
+ "restore_success": "🔥",
"restore_error": "❌"
}
diff --git a/app/services/user_service.py b/app/services/user_service.py
index 2166e251..c3f76409 100644
--- a/app/services/user_service.py
+++ b/app/services/user_service.py
@@ -3,7 +3,6 @@ from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import delete, select, update
-
from app.database.crud.user import (
get_user_by_id, get_user_by_telegram_id, get_users_list,
get_users_count, get_users_statistics, get_inactive_users,
@@ -12,8 +11,10 @@ from app.database.crud.user import (
from app.database.crud.transaction import get_user_transactions_count
from app.database.crud.subscription import get_subscription_by_user_id
from app.database.models import (
- User, UserStatus, Subscription, Transaction, PromoCodeUse,
- ReferralEarning, SubscriptionServer, YooKassaPayment, BroadcastHistory, CryptoBotPayment
+ User, UserStatus, Subscription, Transaction, PromoCode, PromoCodeUse,
+ ReferralEarning, SubscriptionServer, YooKassaPayment, BroadcastHistory,
+ CryptoBotPayment, SubscriptionConversion, UserMessage, WelcomeText,
+ SentNotification
)
from app.config import settings
@@ -246,55 +247,103 @@ class UserService:
except Exception as e:
logger.warning(f"⚠️ Ошибка деактивации RemnaWave: {e}")
+
try:
- from app.database.models import UserMessage
- from sqlalchemy import update
-
- result = await db.execute(
- update(UserMessage)
- .where(UserMessage.created_by == user_id)
- .values(created_by=None)
+ sent_notifications_result = await db.execute(
+ select(SentNotification).where(SentNotification.user_id == user_id)
)
- if result.rowcount > 0:
- logger.info(f"🔄 Обновлено {result.rowcount} пользовательских сообщений")
- await db.flush()
- except Exception as e:
- logger.error(f"❌ Ошибка обновления пользовательских сообщений: {e}")
-
- try:
- from app.database.models import PromoCode
- from sqlalchemy import update
+ sent_notifications = sent_notifications_result.scalars().all()
- result = await db.execute(
- update(PromoCode)
- .where(PromoCode.created_by == user_id)
- .values(created_by=None)
- )
- if result.rowcount > 0:
- logger.info(f"🔄 Обновлено {result.rowcount} промокодов")
- await db.flush()
+ if sent_notifications:
+ logger.info(f"🔄 Удаляем {len(sent_notifications)} уведомлений")
+ await db.execute(
+ delete(SentNotification).where(SentNotification.user_id == user_id)
+ )
+ await db.flush()
except Exception as e:
- logger.error(f"❌ Ошибка обновления промокодов: {e}")
+ logger.error(f"❌ Ошибка удаления уведомлений: {e}")
try:
- from app.database.models import WelcomeText
- from sqlalchemy import update
-
- result = await db.execute(
- update(WelcomeText)
- .where(WelcomeText.created_by == user_id)
- .values(created_by=None)
- )
- if result.rowcount > 0:
- logger.info(f"🔄 Обновлено {result.rowcount} приветственных текстов")
- await db.flush()
+ if user.subscription:
+ subscription_servers_result = await db.execute(
+ select(SubscriptionServer).where(
+ SubscriptionServer.subscription_id == user.subscription.id
+ )
+ )
+ subscription_servers = subscription_servers_result.scalars().all()
+
+ if subscription_servers:
+ logger.info(f"🔄 Удаляем {len(subscription_servers)} связей подписка-сервер")
+ await db.execute(
+ delete(SubscriptionServer).where(
+ SubscriptionServer.subscription_id == user.subscription.id
+ )
+ )
+ await db.flush()
except Exception as e:
- logger.error(f"❌ Ошибка обновления приветственных текстов: {e}")
+ logger.error(f"❌ Ошибка удаления связей подписка-сервер: {e}")
try:
- from app.database.models import YooKassaPayment
- from sqlalchemy import select
+ conversions_result = await db.execute(
+ select(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
+ )
+ conversions = conversions_result.scalars().all()
+ if conversions:
+ logger.info(f"🔄 Удаляем {len(conversions)} записей конверсий")
+ await db.execute(
+ delete(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
+ )
+ await db.flush()
+ except Exception as e:
+ logger.error(f"❌ Ошибка удаления записей конверсий: {e}")
+
+ try:
+ 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(f"🔄 Удаляем {len(referral_earnings)} реферальных доходов")
+ await db.execute(
+ delete(ReferralEarning).where(ReferralEarning.user_id == user_id)
+ )
+ await db.flush()
+ except Exception as e:
+ logger.error(f"❌ Ошибка удаления реферальных доходов: {e}")
+
+ try:
+ 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(f"🔄 Удаляем {len(referral_records)} записей о рефералах")
+ await db.execute(
+ delete(ReferralEarning).where(ReferralEarning.referral_id == user_id)
+ )
+ await db.flush()
+ except Exception as e:
+ logger.error(f"❌ Ошибка удаления записей о рефералах: {e}")
+
+ try:
+ 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(f"🔄 Удаляем {len(promocode_uses)} использований промокодов")
+ await db.execute(
+ delete(PromoCodeUse).where(PromoCodeUse.user_id == user_id)
+ )
+ await db.flush()
+ except Exception as e:
+ logger.error(f"❌ Ошибка удаления использований промокодов: {e}")
+
+ try:
yookassa_result = await db.execute(
select(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
)
@@ -306,14 +355,10 @@ class UserService:
delete(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
)
await db.flush()
- logger.info(f"✅ YooKassa платежи удалены")
except Exception as e:
logger.error(f"❌ Ошибка удаления YooKassa платежей: {e}")
try:
- from app.database.models import CryptoBotPayment
- from sqlalchemy import select, delete
-
cryptobot_result = await db.execute(
select(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id)
)
@@ -325,10 +370,9 @@ class UserService:
delete(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id)
)
await db.flush()
- logger.info(f"✅ CryptoBot платежи удалены")
except Exception as e:
logger.error(f"❌ Ошибка удаления CryptoBot платежей: {e}")
-
+
try:
transactions_result = await db.execute(
select(Transaction).where(Transaction.user_id == user_id)
@@ -341,89 +385,72 @@ class UserService:
delete(Transaction).where(Transaction.user_id == user_id)
)
await db.flush()
- logger.info(f"✅ Транзакции удалены")
except Exception as e:
logger.error(f"❌ Ошибка удаления транзакций: {e}")
-
+
try:
- await db.execute(
- delete(PromoCodeUse).where(PromoCodeUse.user_id == user_id)
- )
- await db.flush()
- logger.info(f"🗑️ Удалены использования промокодов пользователя {user_id}")
- except Exception as e:
- logger.error(f"❌ Ошибка удаления использований промокодов: {e}")
-
- try:
- await db.execute(
- delete(ReferralEarning).where(ReferralEarning.user_id == user_id)
- )
- await db.flush()
- logger.info(f"🗑️ Удалены реферальные доходы пользователя {user_id}")
- except Exception as e:
- logger.error(f"❌ Ошибка удаления реферальных доходов: {e}")
-
- try:
- await db.execute(
- delete(ReferralEarning).where(ReferralEarning.referral_id == user_id)
- )
- await db.flush()
- logger.info(f"🗑️ Удалены реферальные записи о пользователе {user_id}")
- except Exception as e:
- logger.error(f"❌ Ошибка удаления реферальных записей: {e}")
-
- try:
- from app.database.models import BroadcastHistory
- await db.execute(
- delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id)
- )
- await db.flush()
- logger.info(f"🗑️ Удалена история рассылок админа {user_id}")
- except Exception as e:
- logger.error(f"❌ Ошибка удаления истории рассылок: {e}")
-
- try:
- from app.database.models import SubscriptionConversion
- conversions_result = await db.execute(
- select(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
- )
- conversions = conversions_result.scalars().all()
-
- if conversions:
- logger.info(f"🔄 Удаляем {len(conversions)} записей конверсий")
- await db.execute(
- delete(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
- )
- await db.flush()
- logger.info(f"✅ Записи конверсий удалены")
- except Exception as e:
- logger.error(f"❌ Ошибка удаления записей конверсий: {e}")
-
- if user.subscription:
- try:
- await db.execute(
- delete(SubscriptionServer).where(
- SubscriptionServer.subscription_id == user.subscription.id
- )
- )
- await db.flush()
- logger.info(f"🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
- except Exception as e:
- logger.error(f"❌ Ошибка удаления SubscriptionServer: {e}")
-
- if user.subscription:
- try:
- from app.database.models import Subscription
+ if user.subscription:
+ logger.info(f"🔄 Удаляем подписку {user.subscription.id}")
await db.execute(
delete(Subscription).where(Subscription.user_id == user_id)
)
await db.flush()
- logger.info(f"🗑️ Удалена подписка пользователя {user_id}")
- except Exception as e:
- logger.error(f"❌ Ошибка удаления подписки: {e}")
+ except Exception as e:
+ logger.error(f"❌ Ошибка удаления подписки: {e}")
+
try:
- from sqlalchemy import update
+ 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(f"🔄 Обновлено {user_messages_result.rowcount} пользовательских сообщений")
+ await db.flush()
+ except Exception as e:
+ logger.error(f"❌ Ошибка обновления пользовательских сообщений: {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(f"🔄 Обновлено {promocodes_result.rowcount} промокодов")
+ await db.flush()
+ except Exception as e:
+ logger.error(f"❌ Ошибка обновления промокодов: {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(f"🔄 Обновлено {welcome_texts_result.rowcount} приветственных текстов")
+ await db.flush()
+ except Exception as e:
+ logger.error(f"❌ Ошибка обновления приветственных текстов: {e}")
+
+ try:
+ 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(f"🔄 Удаляем {len(broadcast_history)} записей истории рассылок")
+ await db.execute(
+ delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id)
+ )
+ await db.flush()
+ except Exception as e:
+ logger.error(f"❌ Ошибка удаления истории рассылок: {e}")
+
+ try:
referrals_result = await db.execute(
update(User)
.where(User.referred_by_id == user_id)
@@ -434,7 +461,7 @@ class UserService:
await db.flush()
except Exception as e:
logger.error(f"❌ Ошибка очистки реферальных ссылок: {e}")
-
+
try:
await db.execute(
delete(User).where(User.id == user_id)
diff --git a/app/utils/payment_utils.py b/app/utils/payment_utils.py
new file mode 100644
index 00000000..78bf2d79
--- /dev/null
+++ b/app/utils/payment_utils.py
@@ -0,0 +1,123 @@
+from typing import List, Dict, Tuple
+from app.config import settings
+
+def get_available_payment_methods() -> List[Dict[str, str]]:
+ """
+ Возвращает список доступных способов оплаты с их настройками
+ """
+ methods = []
+
+ if settings.TELEGRAM_STARS_ENABLED:
+ methods.append({
+ "id": "stars",
+ "name": "Telegram Stars",
+ "icon": "⭐",
+ "description": "быстро и удобно",
+ "callback": "topup_stars"
+ })
+
+ if settings.is_yookassa_enabled():
+ methods.append({
+ "id": "yookassa",
+ "name": "Банковская карта",
+ "icon": "💳",
+ "description": "через YooKassa",
+ "callback": "topup_yookassa"
+ })
+
+ if settings.TRIBUTE_ENABLED:
+ methods.append({
+ "id": "tribute",
+ "name": "Банковская карта",
+ "icon": "💳",
+ "description": "через Tribute",
+ "callback": "topup_tribute"
+ })
+
+ if settings.is_cryptobot_enabled():
+ methods.append({
+ "id": "cryptobot",
+ "name": "Криптовалюта",
+ "icon": "🪙",
+ "description": "через CryptoBot",
+ "callback": "topup_cryptobot"
+ })
+
+ # Поддержка всегда доступна
+ methods.append({
+ "id": "support",
+ "name": "Через поддержку",
+ "icon": "🛠️",
+ "description": "другие способы",
+ "callback": "topup_support"
+ })
+
+ return methods
+
+def get_payment_methods_text() -> str:
+ """
+ Генерирует текст с описанием доступных способов оплаты
+ """
+ methods = get_available_payment_methods()
+
+ if len(methods) <= 1: # Только поддержка
+ return """💳 Способы пополнения баланса
+
+⚠️ В данный момент автоматические способы оплаты временно недоступны.
+Обратитесь в техподдержку для пополнения баланса.
+
+Выберите способ пополнения:"""
+
+ text = "💳 Способы пополнения баланса\n\n"
+ text += "Выберите удобный для вас способ оплаты:\n\n"
+
+ for method in methods:
+ text += f"{method['icon']} {method['name']} - {method['description']}\n"
+
+ text += "\nВыберите способ пополнения:"
+
+ return text
+
+def is_payment_method_available(method_id: str) -> bool:
+ """
+ Проверяет, доступен ли конкретный способ оплаты
+ """
+ if method_id == "stars":
+ return settings.TELEGRAM_STARS_ENABLED
+ elif method_id == "yookassa":
+ return settings.is_yookassa_enabled()
+ elif method_id == "tribute":
+ return settings.TRIBUTE_ENABLED
+ elif method_id == "cryptobot":
+ return settings.is_cryptobot_enabled()
+ elif method_id == "support":
+ return True # Поддержка всегда доступна
+ else:
+ return False
+
+def get_payment_method_status() -> Dict[str, bool]:
+ """
+ Возвращает статус всех способов оплаты
+ """
+ return {
+ "stars": settings.TELEGRAM_STARS_ENABLED,
+ "yookassa": settings.is_yookassa_enabled(),
+ "tribute": settings.TRIBUTE_ENABLED,
+ "cryptobot": settings.is_cryptobot_enabled(),
+ "support": True
+ }
+
+def get_enabled_payment_methods_count() -> int:
+ """
+ Возвращает количество включенных способов оплаты (не считая поддержку)
+ """
+ count = 0
+ if settings.TELEGRAM_STARS_ENABLED:
+ count += 1
+ if settings.is_yookassa_enabled():
+ count += 1
+ if settings.TRIBUTE_ENABLED:
+ count += 1
+ if settings.is_cryptobot_enabled():
+ count += 1
+ return count
\ No newline at end of file