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