# Профиль. последнее изменениеы from aiogram import Router, types, F from aiogram.types import ( CallbackQuery, LabeledPrice, Message, PreCheckoutQuery, ) import logging from datetime import datetime from aiogram.enums.parse_mode import ParseMode import locale from instences.config import BASE_URL_FASTAPI import aiohttp from keyboard.keyboards import ( account_keyboard, popup_keyboard, tranhist_keyboard, confirm_popup_keyboard, guide_keyboard, balance_keyboard, payment_methods_keyboard, ) locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8") router = Router() logger = logging.getLogger(__name__) async def call_api(method, endpoint, data=None, base_url=BASE_URL_FASTAPI): """ Выполняет HTTP-запрос к FastAPI. """ url = f"{base_url}{endpoint}" logger.info(f"Инициализация запроса: {method} {url} с данными {data}") try: async with aiohttp.ClientSession() as session: async with session.request( method, url, json=data, headers={"Content-Type": "application/json"} ) as response: logger.info( f"Получен ответ от {url}: статус {response.status}" ) if response.status in {200, 201}: result = await response.json() logger.debug(f"Ответ JSON: {result}") return result if response.status == 404: logger.debug(f"Код {response.status}, возвращаю ничего") return None logger.error( f"Ошибка в запросе: статус {response.status}, причина {response.reason}" ) return "ERROR" except Exception as e: logger.exception(f"Исключение при выполнении запроса к {url}: {e}") return "ERROR" @router.callback_query(lambda callback: callback.data == "profile") async def profile_callback_handler(callback: CallbackQuery): """ Профиль пользователя. Логика работы с API сохранена, изменён только текст/визуал. """ try: user_data = await call_api("GET", f"/user/{callback.from_user.id}") if not user_data: await callback.message.answer( "Произошла ошибка, попробуйте позже или свяжитесь с администрацией." ) await callback.answer() return sub_data = await call_api( "GET", f"/subscription/{user_data['telegram_id']}/last" ) if sub_data == "ERROR" or not isinstance(sub_data, dict): sub_data = None username = callback.from_user.username or "-" balance = user_data.get("balance", 0) # Статус подписки: Активна / Нет активных sub_status = "⚫ Нет активных" if sub_data: expiry_date = sub_data.get("end_date") if expiry_date: try: is_expired = datetime.fromisoformat( expiry_date) < datetime.now() except ValueError: is_expired = True else: is_expired = True if not is_expired: sub_status = "🟢 Активна" text = ( "🥚 Профиль\n\n" f"Пользователь: @{username}\n" f"Баланс: {balance} ₽\n" f"Статус подписки: {sub_status}\n\n" "Выберите действие:" ) await callback.message.edit_text( text, parse_mode=ParseMode.HTML, reply_markup=account_keyboard(), ) except Exception as e: logger.exception(f"Ошибка в обработчике профиля: {e}") await callback.message.answer("Произошла ошибка. Попробуйте позже.") finally: await callback.answer() @router.callback_query(lambda callback: callback.data == "balance") async def balance_callback_handler(callback: CallbackQuery): """ При нажатии «Пополнить баланс» показываем выбор суммы пополнения. """ await callback.message.edit_text( "💳 Выберите сумму пополнения:", reply_markup=popup_keyboard(), ) await callback.answer() @router.callback_query(lambda callback: callback.data == "popup") async def popup_callback_handler(callback: CallbackQuery): """ Обработчик callback_query для выбора суммы пополнения. """ user = await call_api("GET", f"/user/{callback.from_user.id}") if not user: await callback.message.answer( "Произошла ошибка, попробуйте позже или свяжитесь с администрацией." ) await callback.answer() return await callback.message.edit_text( "Выбери сумму для пополнения баланса.", reply_markup=popup_keyboard(), ) await callback.answer() @router.callback_query(lambda callback: callback.data == "tranhist") async def tranhist_callback_handler(callback: CallbackQuery): """ Обработчик callback_query для истории транзакций. (Логику и формат Markdown_V2 не трогаем, чтобы не поймать новые баги) """ user_data = await call_api("GET", f"/user/{callback.from_user.id}") if not user_data: await callback.message.edit_text("Вы еще не зарегистрированы.") await callback.answer() return try: transactions = await call_api( "GET", f"/user/{user_data['telegram_id']}/transactions" ) if not transactions: await callback.message.edit_text( "У вас нет транзакций.", reply_markup=tranhist_keyboard() ) await callback.answer() return result = "Ваши транзакции:```\n" for count, tran in enumerate(transactions, start=1): dt = datetime.fromisoformat(tran["created_at"]).strftime( "%d.%m.%Y %H:%M:%S" ) result += f"{count}. Сумма: {tran['amount']}, Дата: {dt}\n" if len(result) > 4000: result += "...\nСлишком много транзакций для отображения." break result += "```" await callback.message.edit_text( result, parse_mode=ParseMode.MARKDOWN_V2, reply_markup=tranhist_keyboard(), ) except Exception as e: logger.error(f"Ошибка обработки транзакций: {e}") await callback.message.edit_text( "Произошла ошибка. Попробуйте позже." ) finally: await callback.answer() @router.callback_query(lambda callback: callback.data.startswith("popup:")) async def popup_confirm_callback_handler(callback: CallbackQuery): """ После выбора суммы показываем варианты оплаты. Разрешены только суммы 200, 300, 600, 1000 ₽. """ try: _, amount_raw = callback.data.split(":", maxsplit=1) amount = int(float(amount_raw)) except Exception: await callback.message.answer("Некорректная сумма пополнения.") await callback.answer() return if amount not in {200, 300, 600, 1000}: await callback.message.answer( "Эта сумма пополнения недоступна. Выбери вариант от 200 до 1000 ₽." ) await callback.answer() return text = ( f"💰 Сумма пополнения: {amount} ₽\n\n" "Выбери способ оплаты:" ) await callback.message.edit_text( text=text, reply_markup=payment_methods_keyboard(amount), ) await callback.answer() # ===== Telegram Stars ===== @router.callback_query(lambda callback: callback.data.startswith("method_stars_")) async def method_stars_handler(callback: CallbackQuery): """ Оплата через Telegram Stars. Формируем invoice прямо из бота, без отдельного биллинга. """ try: amount_str = callback.data.split("_")[-1] amount_rub = int(float(amount_str)) except Exception: await callback.message.answer("Некорректная сумма для оплаты.") await callback.answer() return payload = f"stars_topup:{callback.from_user.id}:{amount_rub}" stars_amount = amount_rub prices = [ LabeledPrice( label=f"Пополнение баланса на {amount_rub} ₽", amount=stars_amount, ) ] try: await callback.message.answer_invoice( title="Пополнение баланса Lark VPN", description=( f"Пополнение баланса на {amount_rub} ₽ через Telegram Stars.\n\n" "После успешной оплаты баланс будет зачислен автоматически." ), payload=payload, provider_token="", # для Stars провайдер пустой currency="XTR", prices=prices, ) await callback.answer() except Exception as e: logger.exception(f"Ошибка при отправке invoice Telegram Stars: {e}") await callback.message.answer( "Не удалось создать счёт в Telegram Stars. Попробуй позже или выбери другой способ оплаты." ) await callback.answer() @router.callback_query(lambda callback: callback.data.startswith("method_ykassa_")) async def method_ykassa_handler(callback: CallbackQuery): """ Обработчик оплаты через YooKassa. """ amount = callback.data.split("_")[-1] try: await callback.answer() except Exception: pass endpoint = ( f"/billing/payments/init?" f"user_id={callback.from_user.id}&amount={float(amount)}&provider=yookassa" ) logger.info(f"Отправка запроса на инициализацию платежа: {endpoint}") result = await call_api("POST", endpoint, None, "http://billing:8000") if result == "ERROR" or not isinstance(result, dict): await callback.message.edit_text( "❌ Произошла ошибка при создании платежа. Попробуйте позже." ) return if not result.get("success", False): error_msg = ( result.get("error") or result.get("detail") or "Неизвестная ошибка" ) await callback.message.edit_text(f"❌ Ошибка: {error_msg}") return payment_url = result.get("confirmation_url", "#") payment_id = result.get("payment_id", "") await callback.message.edit_text( f"💵 Оплата через YooKassa\n\n" f"💰 Сумма: {amount} руб\n" f"📋 ID платежа: {payment_id}\n\n" f"➡️ Перейти к оплате\n\n" f"После оплаты нажмите кнопку 'Проверить оплату'", parse_mode=ParseMode.HTML, disable_web_page_preview=True, reply_markup=types.InlineKeyboardMarkup( inline_keyboard=[[ types.InlineKeyboardButton( text="🔄 Проверить оплату", callback_data=f"check_payment:{payment_id}", ) ]] ), ) await callback.answer() @router.callback_query(lambda callback: callback.data.startswith("method_crypto_")) async def method_crypto_handler(callback: CallbackQuery): """ Оплата через CryptoBot. """ amount = callback.data.split("_")[-1] try: await callback.answer() except Exception: pass endpoint = ( f"/billing/payments/init?" f"user_id={callback.from_user.id}&amount={float(amount)}&provider=cryptobot" ) logger.info(f"Отправка запроса на инициализацию платежа: {endpoint}") result = await call_api("POST", endpoint, None, "http://billing:8000") if result == "ERROR" or not isinstance(result, dict): await callback.message.edit_text( "❌ Произошла ошибка при создании платежа. Попробуйте позже." ) return if not result.get("success", False): error_msg = ( result.get("error") or result.get("detail") or "Неизвестная ошибка" ) await callback.message.edit_text(f"❌ Ошибка: {error_msg}") return payment_url = result.get("confirmation_url", "#") payment_id = result.get("payment_id", "") await callback.message.edit_text( f"💵 Оплата через Сryptobot\n\n" f"💰 Сумма: {amount} руб\n" f"📋 ID платежа: {payment_id}\n\n" f"➡️ Перейти к оплате\n\n" f"После оплаты нажмите кнопку 'Проверить оплату'", parse_mode=ParseMode.HTML, disable_web_page_preview=True, reply_markup=types.InlineKeyboardMarkup( inline_keyboard=[[ types.InlineKeyboardButton( text="🔄 Проверить оплату", callback_data=f"check_payment:{payment_id}", ) ]] ), ) await callback.answer() @router.callback_query(lambda callback: callback.data.startswith("method_sbp_")) async def method_sbp_handler(callback: types.CallbackQuery): try: amount = int(callback.data.split("_")[2]) except Exception: await callback.message.answer("Ошибка суммы платежа.") await callback.answer() return endpoint = ( f"/billing/payments/init?" f"user_id={callback.from_user.id}&amount={float(amount)}&provider=sbp" ) result = await call_api("POST", endpoint,None ,"http://billing:8000") if result == "ERROR" or not isinstance(result, dict): await callback.message.answer("Ошибка при инициализации платежа.") await callback.answer() return url = result.get("confirmation_url") if not url: await callback.message.answer("СБП временно недоступно.") await callback.answer() return await callback.message.answer( f"🏦 Платёж через СБП:\nПерейди по ссылке для оплаты:\n{url}" ) await callback.answer() @router.callback_query(lambda callback: callback.data == "guide") async def guide_callback_handler(callback: CallbackQuery): """ Обработчик callback_query для руководства. """ await callback.message.edit_text( "Выбери платформу, для которой нужно руководство по подключению:", reply_markup=guide_keyboard(), ) await callback.answer() # ===== Служебные хендлеры для платежей Telegram Stars ===== @router.pre_checkout_query() async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery): """ Обязательный шаг для Telegram Payments: подтверждаем pre_checkout_query, иначе платёж не пройдёт. """ try: await pre_checkout_query.bot.answer_pre_checkout_query( pre_checkout_query.id, ok=True, ) except Exception as e: logger.exception(f"Ошибка при answer_pre_checkout_query: {e}") async def _process_stars_topup(message: Message): """ Логика зачисления средств после успешной оплаты Stars. """ sp = message.successful_payment if not sp: return payload = sp.invoice_payload or "" parts = payload.split(":") if len(parts) != 3 or parts[0] != "stars_topup": logger.info( "successful_payment не относится к пополнению баланса Stars.") return _, telegram_id_str, amount_str = parts try: amount_rub = int(amount_str) except ValueError: # На всякий случай fallback к total_amount amount_rub = sp.total_amount data = { "telegram_id": telegram_id_str, "amount": amount_rub, "currency": sp.currency, "provider": "telegram_stars", "telegram_payment_charge_id": sp.telegram_payment_charge_id, } logger.info( f"Обработка успешного платежа Telegram Stars: " f"user={telegram_id_str}, amount={amount_rub}, currency={sp.currency}" ) result = await call_api("POST", "/user/deposit", data) if result == "ERROR" or result is None: await message.answer( "⭐ Оплата через Telegram Stars прошла, но не удалось автоматически обновить баланс.\n" "Если баланс не изменился — напиши, пожалуйста, в поддержку и укажи время платежа." ) return await message.answer( f"⭐ Оплата через Telegram Stars успешно проведена.\n" f"На твой баланс зачислено {amount_rub} ₽." ) @router.message(F.successful_payment) async def successful_payment_handler(message: Message): """ Глобальный хендлер успешных платежей. Сейчас используем только для пополнения баланса через Stars. """ try: await _process_stars_topup(message) except Exception as e: logger.exception(f"Ошибка при обработке successful_payment: {e}") await message.answer( "Оплата прошла, но произошла ошибка при обработке. " "Если баланс не изменился — напиши, пожалуйста, в поддержку." )