From 79307b9791b7e92888b863ee670c24a93121e6a6 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 24 Nov 2025 23:48:29 +0300 Subject: [PATCH 01/13] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D1=91=D0=BD=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=84=D0=B8=D0=BB=D1=8F=20=D0=B8=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D1=83=D1=81=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/profile.py | 91 +++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/handlers/profile.py b/handlers/profile.py index 940ec51..cf005da 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -1,3 +1,5 @@ +# Профиль. последнее изменение 24.11.2025 + from aiogram import Router, types from aiogram.types import CallbackQuery import logging @@ -32,7 +34,8 @@ async def call_api(method, endpoint, data=None): async with aiohttp.ClientSession() as session: async with session.request(method, url, json=data) as response: logger.info( - f"Получен ответ от {url}: статус {response.status}") + f"Получен ответ от {url}: статус {response.status}" + ) if response.status in {200, 201}: result = await response.json() @@ -54,7 +57,7 @@ async def call_api(method, endpoint, data=None): async def profile_callback_handler(callback: CallbackQuery): """ Профиль пользователя. - Логика работы с API сохранена, переписан только текст/визуал. + Логика работы с API сохранена, изменён только текст/визуал (как в профиле Михаила). """ try: user_data = await call_api("GET", f"/user/{callback.from_user.id}") @@ -65,55 +68,46 @@ async def profile_callback_handler(callback: CallbackQuery): await callback.answer() return - sub_data = await call_api("GET", f"/subscription/{user_data['id']}/last") + # Последняя подписка пользователя + sub_data = await call_api( + "GET", f"/subscription/{user_data['id']}/last" + ) if sub_data == "ERROR" or not isinstance(sub_data, dict): sub_data = None - balance_text = f"💰 Баланс: {user_data['balance']} ₽" + username = callback.from_user.username or "-" + balance = user_data.get("balance", 0) - username = callback.from_user.username or "пользователь" + # Определяем статус подписки + sub_status = "⚫ Нет активных" - # Нет подписки - if not sub_data: - text = ( - f"📜 Профиль {username}\n\n" - f"{balance_text}\n\n" - "У тебя пока нет активной подписки.\n" - "Пополняй баланс и подключай тариф Lark, чтобы получить статус 🐣 или 🦅." - ) - else: + if sub_data: expiry_date = sub_data.get("expiry_date") - formatted_date = ( - datetime.fromisoformat(expiry_date).strftime("%d %B %Y г.") - if expiry_date - else None - ) - is_expired = ( - datetime.fromisoformat(expiry_date) < datetime.now() - if expiry_date - else True - ) - status_icon = "✖️" if is_expired else "☑️" - profile_status = "🦅" if "Pro" in sub_data.get("plan", "") else "🐣" - - if not is_expired and formatted_date: - sub_text = ( - f"Подписка активна до {formatted_date} {status_icon}\n" - f"Текущий статус: {profile_status}" - ) + if expiry_date: + try: + is_expired = datetime.fromisoformat( + expiry_date) < datetime.now() + except ValueError: + is_expired = True else: - sub_text = ( - f"Подписка неактивна {status_icon}\n" - "Ты можешь продлить её в разделе подписок." - ) + is_expired = True - text = ( - f"📜 Профиль {username} {profile_status}\n\n" - f"{sub_text}\n\n" - f"{balance_text}" - ) + if not is_expired: + sub_status = "🟢 Активна" - await callback.message.edit_text(text, reply_markup=account_keyboard()) + 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("Произошла ошибка. Попробуйте позже.") @@ -175,7 +169,9 @@ async def tranhist_callback_handler(callback: CallbackQuery): return try: - transactions = await call_api("GET", f"/user/{user_data['id']}/transactions") + transactions = await call_api( + "GET", f"/user/{user_data['id']}/transactions" + ) if not transactions: await callback.message.edit_text( "У вас нет транзакций.", reply_markup=tranhist_keyboard() @@ -200,7 +196,9 @@ async def tranhist_callback_handler(callback: CallbackQuery): ) except Exception as e: logger.error(f"Ошибка обработки транзакций: {e}") - await callback.message.edit_text("Произошла ошибка. Попробуйте позже.") + await callback.message.edit_text( + "Произошла ошибка. Попробуйте позже." + ) finally: await callback.answer() @@ -223,7 +221,10 @@ async def popup_confirm_callback_handler(callback: CallbackQuery): await callback.answer() return - text = f"✅ Баланс пополнен на {popup_info} ₽. Спасибо, что остаёшься с Lark." + text = ( + f"✅ Баланс пополнен на {popup_info} ₽. " + "Спасибо, что остаёшься с Lark." + ) await callback.message.edit_text( text=text, reply_markup=confirm_popup_keyboard() ) From cda94583ed56400a29e72c2ff63970eaeb25f339 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 25 Nov 2025 00:11:33 +0300 Subject: [PATCH 02/13] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=B4=D0=B2=D0=B5=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B8,=20?= =?UTF-8?q?=D0=BA=D0=B0=D0=BA=20=D1=83=20=D0=BC=D0=B5=D0=BD=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyboard/keyboards.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/keyboard/keyboards.py b/keyboard/keyboards.py index f4f78ae..d05fb27 100644 --- a/keyboard/keyboards.py +++ b/keyboard/keyboards.py @@ -20,11 +20,7 @@ def main_keyboard(): return builder.as_markup() -def account_keyboard(): - """ - Аккаунт / профиль - Визуал — твой, callback_data — как у Вовы. - """ +def balance_keyboard(): builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( @@ -32,18 +28,6 @@ def account_keyboard(): callback_data="popup", ) ) - builder.row( - InlineKeyboardButton( - text="🦴 Мои подписки", - callback_data="buy_subscription", - ) - ) - builder.row( - InlineKeyboardButton( - text="📡 Руководство по подключению", - callback_data="guide", - ) - ) builder.row( InlineKeyboardButton( text="🧾 История транзакций", @@ -53,7 +37,7 @@ def account_keyboard(): builder.row( InlineKeyboardButton( text="🔙 Назад", - callback_data="base", + callback_data="profile", ) ) return builder.as_markup() From c7ded9e49968a2906c88962198fd382cef071219 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 25 Nov 2025 07:32:55 +0300 Subject: [PATCH 03/13] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D1=91=D0=BD=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20=D1=81?= =?UTF-8?q?=D1=83=D0=BC=D0=BC=20=D0=BF=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyboard/keyboards.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/keyboard/keyboards.py b/keyboard/keyboards.py index d05fb27..0794bff 100644 --- a/keyboard/keyboards.py +++ b/keyboard/keyboards.py @@ -138,21 +138,16 @@ def subhist_keyboard(): def popup_keyboard(): """ - Пополнение (суммы, стиль как в твоём topup_menu) + Пополнение: суммы как в твоём топап-меню. """ builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="200 ₽", callback_data="popup:200"), - InlineKeyboardButton(text="500 ₽", callback_data="popup:500"), - ) - builder.row( - InlineKeyboardButton(text="1000 ₽", callback_data="popup:1000"), - InlineKeyboardButton(text="2000 ₽", callback_data="popup:2000"), - ) - builder.row( - InlineKeyboardButton(text="3000 ₽", callback_data="popup:3000"), - InlineKeyboardButton(text="5000 ₽", callback_data="popup:5000"), - ) + for amount in [200, 300, 600, 1000]: + builder.row( + InlineKeyboardButton( + text=f"{amount} ₽", + callback_data=f"popup:{amount}", + ) + ) builder.row( InlineKeyboardButton( text="🔙 Назад", @@ -162,6 +157,7 @@ def popup_keyboard(): return builder.as_markup() + def balance_keyboard(): """ Баланс From 779f13b98ec0898209f873fdebcc83c5d49502f3 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 25 Nov 2025 07:55:27 +0300 Subject: [PATCH 04/13] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D1=91=D0=BD=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=B8=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B1=D0=BE=D1=80=D0=B0=20=D1=81=D0=BF=D0=BE=D1=81=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=20=D0=BE=D0=BF=D0=BB=D0=B0=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/profile.py | 71 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/handlers/profile.py b/handlers/profile.py index cf005da..bd10299 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -15,6 +15,7 @@ from keyboard.keyboards import ( confirm_popup_keyboard, guide_keyboard, balance_keyboard, + payment_methods_keyboard, ) locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8") @@ -57,7 +58,7 @@ async def call_api(method, endpoint, data=None): async def profile_callback_handler(callback: CallbackQuery): """ Профиль пользователя. - Логика работы с API сохранена, изменён только текст/визуал (как в профиле Михаила). + Логика работы с API сохранена, изменён только текст/визуал. """ try: user_data = await call_api("GET", f"/user/{callback.from_user.id}") @@ -78,7 +79,7 @@ async def profile_callback_handler(callback: CallbackQuery): username = callback.from_user.username or "-" balance = user_data.get("balance", 0) - # Определяем статус подписки + # Статус подписки: Активна / Нет активных sub_status = "⚫ Нет активных" if sub_data: @@ -206,27 +207,65 @@ async def tranhist_callback_handler(callback: CallbackQuery): @router.callback_query(lambda callback: callback.data.startswith("popup:")) async def popup_confirm_callback_handler(callback: CallbackQuery): """ - Обработчик подтверждения пополнения баланса. + После выбора суммы показываем варианты оплаты. + Реального списания пока нет — только выбор метода. """ - data = callback.data.split(":") - popup_info = data[1] - result = await call_api( - "POST", - f"/user/{callback.from_user.id}/balance/{float(popup_info)}", - ) - if result == "ERROR": - await callback.message.answer( - "Произошла ошибка, попробуйте позже или свяжитесь с администрацией." - ) + try: + data = callback.data.split(":") + amount_str = data[1] + amount = int(float(amount_str)) + except Exception: + await callback.message.answer("Некорректная сумма пополнения.") await callback.answer() return text = ( - f"✅ Баланс пополнен на {popup_info} ₽. " - "Спасибо, что остаёшься с Lark." + f"💰 Сумма пополнения: {amount} ₽\n\n" + "Выбери способ оплаты:" ) + await callback.message.edit_text( - text=text, reply_markup=confirm_popup_keyboard() + text=text, + reply_markup=payment_methods_keyboard(amount), + ) + await callback.answer() + + +@router.callback_query(lambda callback: callback.data.startswith("method_stars_")) +async def method_stars_handler(callback: CallbackQuery): + """ + Заглушка: оплата через Telegram Stars. + """ + amount = callback.data.split("_")[-1] + await callback.message.edit_text( + f"⭐ Оплата через Telegram Stars на {amount} ₽ пока в разработке.\n\n" + "Позже сюда подвяжем реальный платёж.", + ) + 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] + await callback.message.edit_text( + f"💵 Оплата через YooKassa на {amount} ₽ пока в разработке.\n\n" + "Функционал появится после настройки биллинга.", + ) + 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] + await callback.message.edit_text( + f"🪙 Оплата через CryptoBot на {amount} ₽ пока в разработке.\n\n" + "Платёжный шлюз будет добавлен позже.", ) await callback.answer() From f042cc2f9e7d511246ccec5cd9a7db875cf6d536 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 25 Nov 2025 15:40:33 +0300 Subject: [PATCH 05/13] =?UTF-8?q?UI:=20=D1=82=D0=B0=D1=80=D0=B8=D1=84?= =?UTF-8?q?=D1=8B=20Basic/Pro/Family=20=D0=B8=20=D0=BC=D0=B5=D0=BD=D1=8E?= =?UTF-8?q?=20=D0=BE=D0=BF=D0=BB=D0=B0=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/subscriptions.py | 73 +++++++++++++++++++++++------- keyboard/keyboards.py | 94 +++++++++++++++++++++++---------------- 2 files changed, 112 insertions(+), 55 deletions(-) diff --git a/handlers/subscriptions.py b/handlers/subscriptions.py index 2305ff1..246c43d 100644 --- a/handlers/subscriptions.py +++ b/handlers/subscriptions.py @@ -4,11 +4,18 @@ from instences.config import BASE_URL_FASTAPI import aiohttp from aiogram.enums.parse_mode import ParseMode from aiogram.filters import Command -from keyboard.keyboards import tarif_Lark_pro_keyboard, tarif_Lark_keyboard, tarif_confirm_keyboard,buy_keyboard +from keyboard.keyboards import ( + tarif_Lark_pro_keyboard, + tarif_Lark_keyboard, + tarif_Lark_family_keyboard, + tarif_confirm_keyboard, + buy_keyboard, +) router = Router() logger = logging.getLogger(__name__) + async def call_api(method, endpoint, data=None): """ Выполняет HTTP-запрос к FastAPI. @@ -19,22 +26,25 @@ async def call_api(method, endpoint, data=None): try: async with aiohttp.ClientSession() as session: async with session.request(method, url, json=data) as response: - logger.info(f"Получен ответ от {url}: статус {response.status}") + 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 in {404,400}: + if response.status in {404, 400}: result = await response.json() logger.debug(f"Код {response.status}, возвращаю {result}") return result - logger.error(f"Ошибка в запросе: статус {response.status}, причина {response.reason}") + logger.error( + f"Ошибка в запросе: статус {response.status}, причина {response.reason}") return "ERROR" except Exception as e: logger.exception(f"Исключение при выполнении запроса к {url}: {e}") return "ERROR" + def escape_markdown_v2(text: str) -> str: """ Экранирует специальные символы для Markdown_V2. @@ -45,7 +55,6 @@ def escape_markdown_v2(text: str) -> str: return text - @router.message(Command("subscriptions")) async def supp(message: types.Message): """ @@ -56,7 +65,8 @@ async def supp(message: types.Message): try: # Вызов API для получения URI result = await call_api("GET", f"/uri?telegram_id={message.from_user.id}") - uri = result.get('detail', "Error") # Получаем URI из ответа или "Error", если ключ отсутствует + # Получаем URI из ответа или "Error", если ключ отсутствует + uri = result.get('detail', "Error") # Проверка результата if uri == "Error": @@ -71,7 +81,8 @@ async def supp(message: types.Message): except Exception as e: # Логирование ошибок logger.error(f"Ошибка при вызове API для подписки: {e}") - text = escape_markdown_v2("Произошла неожиданная ошибка при получении подписки.") + text = escape_markdown_v2( + "Произошла неожиданная ошибка при получении подписки.") # Ответ пользователю await message.answer( @@ -79,36 +90,67 @@ async def supp(message: types.Message): parse_mode=ParseMode.MARKDOWN_V2 ) + @router.callback_query(lambda callback: callback.data == "buy_subscription") async def buy_subscription_callback_handler(callback: types.CallbackQuery): """ - Обработчик callback_query с data="buy_subscription". + Меню выбора тарифа: Basic / Pro / Family + показ баланса. """ - await callback.message.edit_text( - f"Ознакомься с условиями в вкладке \"О тарифах\" и выбери подходящий 🦅", - reply_markup=buy_keyboard() + # Тянем пользователя, чтобы показать баланс + user_data = await call_api("GET", f"/user/{callback.from_user.id}") + balance = user_data.get("balance", 0) if user_data else "?" + + text = ( + "➕ Подключить новую подписку\n\n" + "🐣 Lark Basic - 200 ₽ / мес\n" + "🦅 Lark Pro - 400 ₽ / мес\n" + "👨‍👩‍👧 Lark Family - 700 ₽ / мес\n\n" + f"💰 Баланс: {balance} ₽\n\n" + "Выбери тариф:" ) + await callback.message.edit_text( + text, + parse_mode=ParseMode.HTML, + reply_markup=buy_keyboard(), + ) + await callback.answer() + + @router.callback_query(lambda callback: callback.data == "subs") async def subs_callback_handler(callback: types.CallbackQuery): """ Обработчик callback_query с data="subs". - """ + """ await callback.message.edit_text( "Подписки птенчик", reply_markup=tarif_Lark_keyboard() ) + @router.callback_query(lambda callback: callback.data == "subs_pro") async def subs_pro_callback_handler(callback: types.CallbackQuery): """ Обработчик callback_query с data="subs_pro". - """ + """ await callback.message.edit_text( "Подписки птенчик ПРО", reply_markup=tarif_Lark_pro_keyboard() ) + +@router.callback_query(lambda callback: callback.data == "subs_family") +async def subs_family_callback_handler(callback: types.CallbackQuery): + """ + Обработчик callback_query с data="subs_family". + """ + await callback.message.edit_text( + "Подписки Lark Family", + reply_markup=tarif_Lark_family_keyboard(), + ) + await callback.answer() + + @router.callback_query(lambda callback: callback.data.startswith("Lark:")) async def lark_tariff_callback_handler(callback: types.CallbackQuery): """ @@ -128,11 +170,12 @@ async def lark_tariff_callback_handler(callback: types.CallbackQuery): months = f"{tariff_time} месяцев" text = f"Тариф {tariff_name} на {months}. Продолжите покупку..." - + # Рендеринг клавиатуры keyboard = tarif_confirm_keyboard(tariff_name, tariff_time, tariff_class) await callback.message.edit_text(text=text, reply_markup=keyboard) + @router.callback_query(lambda callback: callback.data.startswith("confirm:")) async def confirm_callback_handler(callback: types.CallbackQuery): """ @@ -164,5 +207,3 @@ async def confirm_callback_handler(callback: types.CallbackQuery): await callback.message.edit_text("Произошла ошибка при оформлении подписки.") finally: await callback.answer() - - diff --git a/keyboard/keyboards.py b/keyboard/keyboards.py index 0794bff..9af09ba 100644 --- a/keyboard/keyboards.py +++ b/keyboard/keyboards.py @@ -92,12 +92,11 @@ def ticket_keyboard(): def buy_keyboard(): """ Меню выбора тарифа. - Лейблы ближе к твоему стилю, но callback’и остаются старые. """ builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( - text="🐣 Lark (Basic)", + text="🐣 Lark Basic", callback_data="subs", ) ) @@ -109,8 +108,8 @@ def buy_keyboard(): ) builder.row( InlineKeyboardButton( - text="ℹ️ О тарифах", - url="https://t.me/proxylark/19", + text="👨‍👩‍👧 Lark Family", + callback_data="subs_family", ) ) builder.row( @@ -136,51 +135,36 @@ def subhist_keyboard(): return builder.as_markup() -def popup_keyboard(): +def payment_methods_keyboard(amount: int): """ - Пополнение: суммы как в твоём топап-меню. + Способы оплаты для выбранной суммы. + ЛаркинсКоины убрал нахер """ builder = InlineKeyboardBuilder() - for amount in [200, 300, 600, 1000]: - builder.row( - InlineKeyboardButton( - text=f"{amount} ₽", - callback_data=f"popup:{amount}", - ) + builder.row( + InlineKeyboardButton( + text="⭐ Telegram Stars", + callback_data=f"method_stars_{amount}", ) + ) + builder.row( + InlineKeyboardButton( + text="💵 YooKassa", + callback_data=f"method_ykassa_{amount}", + ) + ) + builder.row( + InlineKeyboardButton( + text="🪙 CryptoBot", + callback_data=f"method_crypto_{amount}", + ) + ) builder.row( InlineKeyboardButton( text="🔙 Назад", - callback_data="profile", - ) - ) - return builder.as_markup() - - - -def balance_keyboard(): - """ - Баланс - """ - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🪙 Пополнить баланс", callback_data="popup", ) ) - builder.row( - InlineKeyboardButton( - text="🧾 История транзакций", - callback_data="tranhist", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="profile", - ) - ) return builder.as_markup() @@ -248,6 +232,38 @@ def tarif_Lark_pro_keyboard(): return builder.as_markup() +def tarif_Lark_family_keyboard(): + """ + Тариф Lark Family. + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="👨‍👩‍👧 Lark Family 1 месяц", + callback_data="Lark:Family:1", + ) + ) + builder.row( + InlineKeyboardButton( + text="👨‍👩‍👧 Lark Family 6 месяцев", + callback_data="Lark:Family:6", + ) + ) + builder.row( + InlineKeyboardButton( + text="👨‍👩‍👧 Lark Family 12 месяцев", + callback_data="Lark:Family:12", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="buy_subscription", + ) + ) + return builder.as_markup() + + def guide_keyboard(): """ Руководство по подключению From a572c901941d2f0bb42cfb70d7188212c971ac61 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 25 Nov 2025 19:18:22 +0300 Subject: [PATCH 06/13] =?UTF-8?q?UI:=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BA=D0=B8=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B8=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=20=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D1=84=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/subscriptions.py | 509 ++++++++++++++++++++++++++++++-------- keyboard/keyboards.py | 160 ++++++++++-- 2 files changed, 542 insertions(+), 127 deletions(-) diff --git a/handlers/subscriptions.py b/handlers/subscriptions.py index 246c43d..da28b62 100644 --- a/handlers/subscriptions.py +++ b/handlers/subscriptions.py @@ -1,102 +1,376 @@ from aiogram import Router, types -import logging -from instences.config import BASE_URL_FASTAPI -import aiohttp from aiogram.enums.parse_mode import ParseMode from aiogram.filters import Command +import logging +from datetime import datetime +import aiohttp + +from instences.config import BASE_URL_FASTAPI from keyboard.keyboards import ( - tarif_Lark_pro_keyboard, + buy_keyboard, tarif_Lark_keyboard, + tarif_Lark_pro_keyboard, tarif_Lark_family_keyboard, tarif_confirm_keyboard, - buy_keyboard, + subscriptions_card_keyboard, ) router = Router() logger = logging.getLogger(__name__) -async def call_api(method, endpoint, data=None): +async def call_api(method: str, endpoint: str, data: dict | None = None): """ - Выполняет HTTP-запрос к FastAPI. + Унифицированный вызов backend API. """ url = f"{BASE_URL_FASTAPI}{endpoint}" - logger.info(f"Инициализация запроса: {method} {url} с данными {data}") + logger.info(f"API {method} {url} data={data}") try: async with aiohttp.ClientSession() as session: - async with session.request(method, url, json=data) 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 in {404, 400}: - result = await response.json() - logger.debug(f"Код {response.status}, возвращаю {result}") - return result - logger.error( - f"Ошибка в запросе: статус {response.status}, причина {response.reason}") + async with session.request(method, url, json=data) as resp: + logger.info(f"API response {resp.status} {resp.reason}") + if resp.status in {200, 201}: + return await resp.json() + if resp.status in {400, 404}: + # Возвращаем JSON с detail, чтобы разобрать ошибку + try: + return await resp.json() + except Exception: + return None + logger.error(f"Unexpected status {resp.status}: {await resp.text()}") return "ERROR" except Exception as e: - logger.exception(f"Исключение при выполнении запроса к {url}: {e}") + logger.exception(f"API exception {url}: {e}") return "ERROR" def escape_markdown_v2(text: str) -> str: """ - Экранирует специальные символы для Markdown_V2. + Экранирует спецсимволы для Markdown_V2. """ - special_chars = r"_*[]()~`>#+-=|{}.!" - for char in special_chars: - text = text.replace(char, f"\\{char}") + special_chars = r"_*[]()~`>#+-=|{}.!\\" + for ch in special_chars: + text = text.replace(ch, f"\\{ch}") return text -@router.message(Command("subscriptions")) -async def supp(message: types.Message): +def _plan_human_title(plan: str) -> str: """ - Меню системы подписок + Красивое имя тарифа по plan-строке из БД. """ - text = "" - uri = None # Инициализация переменной - try: - # Вызов API для получения URI - result = await call_api("GET", f"/uri?telegram_id={message.from_user.id}") - # Получаем URI из ответа или "Error", если ключ отсутствует - uri = result.get('detail', "Error") + if not plan: + return "—" + if plan.startswith("Lark_Standart"): + return "Lark Basic" + if plan.startswith("Lark_Pro"): + return "Lark Pro" + if plan.startswith("Lark_Family"): + return "Lark Family" + return plan - # Проверка результата - if uri == "Error": - text = escape_markdown_v2("Произошла ошибка при получении URI") - elif uri == "SUB_ERROR": - text = escape_markdown_v2("Вы ещё не приобрели подписки!!") - elif "vless" in uri: - escaped_uri = escape_markdown_v2(uri) # Экранирование URI - text = f"Ваша подписка: ```{escaped_uri}```" + +async def _fetch_user_and_subs(telegram_id: int): + """ + Берём пользователя и список его подписок с backend. + """ + user = await call_api("GET", f"/user/{telegram_id}") + if not user or not isinstance(user, dict): + return None, [] + + user_id = user["id"] + subs = await call_api("GET", f"/subscriptions/{user_id}") + + if subs == "ERROR" or not isinstance(subs, list): + subs = [] + + return user, subs + + +async def _show_subscriptions_view( + msg: types.Message, + telegram_id: int, + index: int = 0, + edit: bool = False, +): + """ + Рендер карточки подписки: навигация + конфиг + продление. + """ + user, subs = await _fetch_user_and_subs(telegram_id) + + if not user: + text = "Не удалось получить данные пользователя. Попробуй чуть позже." + if edit: + await msg.edit_text(text) else: - text = escape_markdown_v2("Произошла ошибка при обработке URI") - except Exception as e: - # Логирование ошибок - logger.error(f"Ошибка при вызове API для подписки: {e}") - text = escape_markdown_v2( - "Произошла неожиданная ошибка при получении подписки.") + await msg.answer(text) + return - # Ответ пользователю - await message.answer( - text, - parse_mode=ParseMode.MARKDOWN_V2 + if not subs: + text = ( + "⚪ У тебя пока нет активных подписок.\n\n" + "Можешь подключить тариф через меню подписок." + ) + if edit: + await msg.edit_text( + text, + parse_mode=ParseMode.HTML, + reply_markup=buy_keyboard(), + ) + else: + await msg.answer( + text, + parse_mode=ParseMode.HTML, + reply_markup=buy_keyboard(), + ) + return + + # Нормализуем индекс + index = max(0, min(index, len(subs) - 1)) + sub = subs[index] + + plan_title = _plan_human_title(sub.get("plan")) + expiry_raw = sub.get("expiry_date") + date_str = "—" + days_left = None + status_emoji = "🟢" + + if expiry_raw: + try: + dt = datetime.fromisoformat(expiry_raw) + date_str = dt.strftime("%d.%m.%Y") + now = datetime.now(dt.tzinfo) if dt.tzinfo else datetime.now() + days_left = (dt.date() - now.date()).days + if days_left < 0: + status_emoji = "⚫" + else: + status_emoji = "🟢" + except ValueError: + status_emoji = "⚪" + + lines = [ + f"{status_emoji} Подписка", + f"Тариф: {plan_title}", + ] + + if days_left is None: + lines.append(f"Действует до {date_str}") + else: + if days_left >= 0: + lines.append(f"Действует до {date_str} (ещё {days_left} дн.)") + else: + lines.append(f"Истекла {date_str}") + + lines.append("") + lines.append(f"{index + 1} из {len(subs)}") + + text = "\n".join(lines) + + kb = subscriptions_card_keyboard( + sub_id=sub["id"], + index=index, + total=len(subs), ) + if edit: + await msg.edit_text(text, parse_mode=ParseMode.HTML, reply_markup=kb) + else: + await msg.answer(text, parse_mode=ParseMode.HTML, reply_markup=kb) + + +# ===== Мои подписки / карточка ===== + +@router.message(Command("subscriptions")) +async def cmd_subscriptions(message: types.Message): + """ + /subscriptions — открыть карточку подписок. + """ + await _show_subscriptions_view( + msg=message, + telegram_id=message.from_user.id, + index=0, + edit=False, + ) + + +@router.callback_query(lambda c: c.data == "go_subscriptions") +async def cb_go_subscriptions(callback: types.CallbackQuery): + """ + Кнопка из других экранов — перейти в 'Мои подписки'. + """ + await _show_subscriptions_view( + msg=callback.message, + telegram_id=callback.from_user.id, + index=0, + edit=True, + ) + await callback.answer() + + +@router.callback_query(lambda c: c.data.startswith("sub_prev:")) +async def cb_sub_prev(callback: types.CallbackQuery): + """ + Навигация: предыдущая подписка. + """ + try: + index = int(callback.data.split(":")[1]) + except (IndexError, ValueError): + index = 0 + + await _show_subscriptions_view( + msg=callback.message, + telegram_id=callback.from_user.id, + index=index, + edit=True, + ) + await callback.answer() + + +@router.callback_query(lambda c: c.data.startswith("sub_next:")) +async def cb_sub_next(callback: types.CallbackQuery): + """ + Навигация: следующая подписка. + """ + try: + index = int(callback.data.split(":")[1]) + except (IndexError, ValueError): + index = 0 + + await _show_subscriptions_view( + msg=callback.message, + telegram_id=callback.from_user.id, + index=index, + edit=True, + ) + await callback.answer() + + +@router.callback_query(lambda c: c.data.startswith("sub_cfg:")) +async def cb_sub_cfg(callback: types.CallbackQuery): + """ + Кнопка 'Конфиг' в карточке подписки. + Тянем URI через /uri?telegram_id=... + """ + result = await call_api( + "GET", + f"/uri?telegram_id={callback.from_user.id}", + ) + + if result == "ERROR" or not isinstance(result, dict): + await callback.message.answer( + "Не удалось получить конфиг. Попробуй позже." + ) + await callback.answer() + return + + detail = result.get("detail") + + if detail == "SUB_ERROR": + await callback.message.answer( + "У тебя нет активной подписки для генерации конфига." + ) + await callback.answer() + return + + if not isinstance(detail, str) or not detail: + await callback.message.answer( + "Что-то пошло не так при выдаче конфига." + ) + await callback.answer() + return + + escaped = escape_markdown_v2(detail) + text = f"Твой конфиг:\n```{escaped}```" + + await callback.message.answer( + text, + parse_mode=ParseMode.MARKDOWN_V2, + ) + await callback.answer() + + +@router.callback_query(lambda c: c.data.startswith("sub_renew:")) +async def cb_sub_renew(callback: types.CallbackQuery): + """ + Кнопка 'Продлить' в карточке подписки. + По факту — повторный /subscription/buy с тем же планом. + """ + sub_id = callback.data.split(":", maxsplit=1)[ + 1] if ":" in callback.data else None + user, subs = await _fetch_user_and_subs(callback.from_user.id) + + if not user or not subs: + await callback.message.answer("Подписка не найдена.") + await callback.answer() + return + + target = next((s for s in subs if s.get("id") == sub_id), None) + if not target: + await callback.message.answer("Подписка не найдена.") + await callback.answer() + return + + plan_id = target.get("plan") + if not plan_id: + await callback.message.answer("Не удалось определить тариф для продления.") + await callback.answer() + return + + result = await call_api( + "POST", + "/subscription/buy", + {"telegram_id": str(callback.from_user.id), "plan_id": plan_id}, + ) + + # Ошибки backend (detail) + if result == "ERROR" or not isinstance(result, dict): + await callback.message.answer("Ошибка при продлении. Попробуй позже.") + await callback.answer() + return + + detail = result.get("detail") + if detail == "INSUFFICIENT_FUNDS": + await callback.message.answer("Недостаточно средств на балансе.") + await callback.answer() + return + if detail == "TARIFF_NOT_FOUND": + await callback.message.answer("Тариф не найден.") + await callback.answer() + return + if detail == "ACTIVE_SUBSCRIPTION_EXISTS": + await callback.message.answer("У тебя уже есть активная подписка этого типа.") + await callback.answer() + return + + # Успех — backend вернёт message с URI + uri = result.get("message") + if isinstance(uri, str) and uri: + escaped_uri = escape_markdown_v2(uri) + await callback.message.answer( + "Подписка успешно продлена.\n" + f"Твой конфиг:\n```{escaped_uri}```", + parse_mode=ParseMode.MARKDOWN_V2, + ) + else: + await callback.message.answer("Подписка успешно продлена.") + + # Перерисовать карточку (обновлённый список) + await _show_subscriptions_view( + msg=callback.message, + telegram_id=callback.from_user.id, + index=0, + edit=True, + ) + await callback.answer() + + +# ===== Меню покупки подписки (Basic / Pro / Family) ===== @router.callback_query(lambda callback: callback.data == "buy_subscription") async def buy_subscription_callback_handler(callback: types.CallbackQuery): """ Меню выбора тарифа: Basic / Pro / Family + показ баланса. """ - # Тянем пользователя, чтобы показать баланс user_data = await call_api("GET", f"/user/{callback.from_user.id}") balance = user_data.get("balance", 0) if user_data else "?" @@ -120,32 +394,34 @@ async def buy_subscription_callback_handler(callback: types.CallbackQuery): @router.callback_query(lambda callback: callback.data == "subs") async def subs_callback_handler(callback: types.CallbackQuery): """ - Обработчик callback_query с data="subs". + Кнопка 'Lark Basic' — раскрываем варианты сроков. """ await callback.message.edit_text( - "Подписки птенчик", - reply_markup=tarif_Lark_keyboard() + "Тариф Lark Basic — выбери срок:", + reply_markup=tarif_Lark_keyboard(), ) + await callback.answer() @router.callback_query(lambda callback: callback.data == "subs_pro") async def subs_pro_callback_handler(callback: types.CallbackQuery): """ - Обработчик callback_query с data="subs_pro". + Кнопка 'Lark Pro'. """ await callback.message.edit_text( - "Подписки птенчик ПРО", - reply_markup=tarif_Lark_pro_keyboard() + "Тариф Lark Pro — выбери срок:", + reply_markup=tarif_Lark_pro_keyboard(), ) + await callback.answer() @router.callback_query(lambda callback: callback.data == "subs_family") async def subs_family_callback_handler(callback: types.CallbackQuery): """ - Обработчик callback_query с data="subs_family". + Кнопка 'Lark Family'. """ await callback.message.edit_text( - "Подписки Lark Family", + "Тариф Lark Family — выбери срок:", reply_markup=tarif_Lark_family_keyboard(), ) await callback.answer() @@ -154,56 +430,83 @@ async def subs_family_callback_handler(callback: types.CallbackQuery): @router.callback_query(lambda callback: callback.data.startswith("Lark:")) async def lark_tariff_callback_handler(callback: types.CallbackQuery): """ - Обработчик для выбора тарифа Lark. + Обработчик выбора конкретного тарифа Lark. + callback_data: Lark:: """ - data = callback.data.split(":") - tariff_name = data[0] - tariff_class = data[1] - tariff_time = int(data[2]) + try: + _, tariff_class, months_str = callback.data.split(":") + months = int(months_str) + except Exception: + await callback.message.answer("Некорректные данные тарифа.") + await callback.answer() + return - # Определение окончания для месяцев - if tariff_time == 1: - months = f"{tariff_time} месяц" - elif 2 <= tariff_time <= 4: - months = f"{tariff_time} месяца" + if months == 1: + months_label = "1 месяц" + elif 2 <= months <= 4: + months_label = f"{months} месяца" else: - months = f"{tariff_time} месяцев" + months_label = f"{months} месяцев" - text = f"Тариф {tariff_name} на {months}. Продолжите покупку..." + text = f"Тариф Lark {tariff_class} на {months_label}. Продолжить покупку?" - # Рендеринг клавиатуры - keyboard = tarif_confirm_keyboard(tariff_name, tariff_time, tariff_class) + keyboard = tarif_confirm_keyboard("Lark", months, tariff_class) await callback.message.edit_text(text=text, reply_markup=keyboard) + await callback.answer() @router.callback_query(lambda callback: callback.data.startswith("confirm:")) async def confirm_callback_handler(callback: types.CallbackQuery): """ - Обработчик подтверждения подписки. + Подтверждение покупки тарифа. """ try: - data = callback.data.split(":")[1] - tariff_info = data.split("_") - plan_id = f"{tariff_info[0]}_{tariff_info[1]}_{tariff_info[2]}" - result = await call_api("POST", "/subscription/buy", {"telegram_id": callback.from_user.id, "plan_id": plan_id}) - detail = result.get("detail", {}) - - if detail == "ERROR": - await callback.message.edit_text("Произошла ошибка при оформлении подписки.") - elif detail == "INSUFFICIENT_FUNDS": - await callback.message.edit_text("Денег на вашем балансе не достаточно.") - elif detail == "TARIFF_NOT_FOUND": - await callback.message.edit_text("Ваш тариф не найден.") - elif detail == "ACTIVE_SUBSCRIPTION_EXISTS": - await callback.message.edit_text("Вы уже имеете активную подписку.") - else: - uri = result.get("message", {}) - escaped_text = escape_markdown_v2(f"Подписка успешно оформлена!") - answer_text = f"Ваш конфиг для подключения: ```{uri}```" - await callback.message.edit_text(escaped_text) - await callback.message.answer(answer_text, parse_mode=ParseMode.MARKDOWN_V2) - except Exception as e: - logger.exception(f"Ошибка при обработке подтверждения подписки: {e}") - await callback.message.edit_text("Произошла ошибка при оформлении подписки.") - finally: + data = callback.data.split(":", maxsplit=1)[1] + name, classif, amount_str = data.split("_") + plan_id = f"{name}_{classif}_{amount_str}" + except Exception: + await callback.message.edit_text("Некорректные данные тарифа.") await callback.answer() + return + + result = await call_api( + "POST", + "/subscription/buy", + {"telegram_id": str(callback.from_user.id), "plan_id": plan_id}, + ) + + if result == "ERROR" or not isinstance(result, dict): + await callback.message.edit_text("Ошибка при оформлении подписки.") + await callback.answer() + return + + detail = result.get("detail") + + if detail == "INSUFFICIENT_FUNDS": + await callback.message.edit_text( + "Недостаточно средств. Попробуй пополнить баланс." + ) + await callback.answer() + return + if detail == "TARIFF_NOT_FOUND": + await callback.message.edit_text("Тариф не найден.") + await callback.answer() + return + if detail == "ACTIVE_SUBSCRIPTION_EXISTS": + await callback.message.edit_text("У тебя уже есть активная подписка.") + await callback.answer() + return + + uri = result.get("message", "") + if uri: + escaped_uri = escape_markdown_v2(uri) + answer_text = f"Подписка успешно оформлена!\n\nТвой конфиг:\n```{escaped_uri}```" + await callback.message.edit_text("Подписка успешно оформлена.") + await callback.message.answer( + answer_text, + parse_mode=ParseMode.MARKDOWN_V2, + ) + else: + await callback.message.edit_text("Подписка успешно оформлена.") + + await callback.answer() diff --git a/keyboard/keyboards.py b/keyboard/keyboards.py index 9af09ba..f5afcc4 100644 --- a/keyboard/keyboards.py +++ b/keyboard/keyboards.py @@ -1,3 +1,5 @@ +from aiogram.types import InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder from aiogram.types import InlineKeyboardButton, KeyboardButton @@ -121,11 +123,136 @@ def buy_keyboard(): return builder.as_markup() -def subhist_keyboard(): +def tarif_Lark_keyboard(): + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="🐣 Lark 1 месяц", + callback_data="Lark:Standart:1", + ) + ) + builder.row( + InlineKeyboardButton( + text="🐣 Lark 6 месяцев", + callback_data="Lark:Standart:6", + ) + ) + builder.row( + InlineKeyboardButton( + text="🐣 Lark 12 месяцев", + callback_data="Lark:Standart:12", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="buy_subscription", + ) + ) + return builder.as_markup() + + +def tarif_Lark_pro_keyboard(): + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="🦅 Lark Pro 1 месяц", + callback_data="Lark:Pro:1", + ) + ) + builder.row( + InlineKeyboardButton( + text="🦅 Lark Pro 6 месяцев", + callback_data="Lark:Pro:6", + ) + ) + builder.row( + InlineKeyboardButton( + text="🦅 Lark Pro 12 месяцев", + callback_data="Lark:Pro:12", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="buy_subscription", + ) + ) + return builder.as_markup() + + +def tarif_Lark_family_keyboard(): + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="👨‍👩‍👧 Lark Family 1 месяц", + callback_data="Lark:Family:1", + ) + ) + builder.row( + InlineKeyboardButton( + text="👨‍👩‍👧 Lark Family 6 месяцев", + callback_data="Lark:Family:6", + ) + ) + builder.row( + InlineKeyboardButton( + text="👨‍👩‍👧 Lark Family 12 месяцев", + callback_data="Lark:Family:12", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="buy_subscription", + ) + ) + return builder.as_markup() + + +def subscriptions_card_keyboard(sub_id: str, index: int, total: int): """ - Подписки — история/список + Карточка подписки: + - навигация ⬅️/➡️ + - 'Конфиг' / 'Продлить' + - 'Новая' / 'Назад' """ builder = InlineKeyboardBuilder() + + nav = [] + if index > 0: + nav.append( + InlineKeyboardButton( + text="⬅️", + callback_data=f"sub_prev:{index-1}", + ) + ) + if index < total - 1: + nav.append( + InlineKeyboardButton( + text="➡️", + callback_data=f"sub_next:{index+1}", + ) + ) + if nav: + builder.row(*nav) + + builder.row( + InlineKeyboardButton( + text="🔑 Конфиг", + callback_data=f"sub_cfg:{sub_id}", + ), + InlineKeyboardButton( + text="🔁 Продлить", + callback_data=f"sub_renew:{sub_id}", + ), + ) + builder.row( + InlineKeyboardButton( + text="➕ Новая", + callback_data="buy_subscription", + ) + ) builder.row( InlineKeyboardButton( text="🔙 Назад", @@ -135,34 +262,18 @@ def subhist_keyboard(): return builder.as_markup() -def payment_methods_keyboard(amount: int): - """ - Способы оплаты для выбранной суммы. - ЛаркинсКоины убрал нахер - """ +def tarif_confirm_keyboard(name: str, amount: int, classif: str): builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( - text="⭐ Telegram Stars", - callback_data=f"method_stars_{amount}", + text="✅ Подтвердить", + callback_data=f"confirm:{name}_{classif}_{amount}", ) ) builder.row( InlineKeyboardButton( - text="💵 YooKassa", - callback_data=f"method_ykassa_{amount}", - ) - ) - builder.row( - InlineKeyboardButton( - text="🪙 CryptoBot", - callback_data=f"method_crypto_{amount}", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="popup", + text="🔙 Отменить", + callback_data="buy_subscription", ) ) return builder.as_markup() @@ -340,7 +451,8 @@ def tarif_confirm_keyboard(name, amount, classif): def confirm_popup_keyboard(): """ - Подтверждение пополнения — без «иди нахуй», мы же под босса господина ларк это красим.... + аааааааааааааааааааааа + """ builder = InlineKeyboardBuilder() builder.row( From cbf113f7bb5868e4c0af8384b95c6de804dd7eda Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 25 Nov 2025 20:20:38 +0300 Subject: [PATCH 07/13] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D0=BC=D0=BC=D0=B0:=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B8=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/__init__.py | 10 ++++++-- handlers/referrals.py | 58 +++++++++++++++++++++++++++++++++++++++++++ keyboard/keyboards.py | 31 +++++++++++++++++++---- main.py | 10 +++++--- 4 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 handlers/referrals.py diff --git a/handlers/__init__.py b/handlers/__init__.py index 697552f..bc1fa73 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -2,6 +2,12 @@ from .start import router as start_router from .profile import router as profile_router from .subscriptions import router as subscriptions_router from .support import router as support_router +from .referrals import router as referrals_router -# Экспортируем все маршрутизаторы -routers = [start_router,profile_router,subscriptions_router,support_router] +routers = [ + start_router, + profile_router, + subscriptions_router, + support_router, + referrals_router, +] diff --git a/handlers/referrals.py b/handlers/referrals.py new file mode 100644 index 0000000..1cf44a3 --- /dev/null +++ b/handlers/referrals.py @@ -0,0 +1,58 @@ +from aiogram import Router, types +from aiogram.filters import Command +from aiogram.enums.parse_mode import ParseMode +import logging + +router = Router() +logger = logging.getLogger(__name__) + + +async def _build_referral_text(bot, user_id: int) -> str: + me = await bot.get_me() + bot_username = me.username or "LarkVPN_bot" + + link = f"https://t.me/{bot_username}?start=ref_{user_id}" + + text = ( + "👥 Реферальная программа\n\n" + "Зови друзей в Lark VPN и получай бонусы на баланс.\n\n" + f"🔗 Твоя ссылка:\n{link}\n\n" + "👤 Приглашено: —\n" + "💰 Начислено бонусов: — ₽\n\n" + "Бонусы падают автоматически, когда приглашённые пополняют баланс." + ) + return text + + +@router.message(Command("referrals")) +async def referrals_command(message: types.Message): + """ + Команда /referrals — показывает текст реферальной программы. + """ + try: + text = await _build_referral_text(message.bot, message.from_user.id) + await message.answer(text, parse_mode=ParseMode.HTML) + except Exception as e: + logger.exception(f"Ошибка в обработчике /referrals: {e}") + await message.answer("Произошла ошибка. Попробуй позже.") + + +@router.callback_query(lambda callback: callback.data == "referral") +async def referrals_callback(callback: types.CallbackQuery): + """ + Кнопка «Реферальная программа» в главном меню. + """ + try: + text = await _build_referral_text( + callback.message.bot, + callback.from_user.id, + ) + await callback.message.edit_text( + text, + parse_mode=ParseMode.HTML, + ) + except Exception as e: + logger.exception(f"Ошибка в обработчике callback 'referral': {e}") + await callback.message.answer("Произошла ошибка. Попробуй позже.") + finally: + await callback.answer() diff --git a/keyboard/keyboards.py b/keyboard/keyboards.py index f5afcc4..b3037c2 100644 --- a/keyboard/keyboards.py +++ b/keyboard/keyboards.py @@ -4,19 +4,40 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder from aiogram.types import InlineKeyboardButton, KeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder +from aiogram.types import InlineKeyboardButton, KeyboardButton + +# ... остальной код выше не трогаем ... + + def main_keyboard(): """ Главное меню (только визуал перетянут под твой стиль) """ builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton( - text="📜 Профиль", callback_data="profile")) - builder.row(InlineKeyboardButton(text="❔ FAQ ❔", callback_data="faq")) + builder.row( + InlineKeyboardButton( + text="📜 Профиль", + callback_data="profile", + ) + ) + builder.row( + InlineKeyboardButton( + text="👥 Реферальная программа", + callback_data="referral", + ) + ) + builder.row( + InlineKeyboardButton( + text="❔ FAQ ❔", + callback_data="faq", + ) + ) # Оставляем URL как у Вовы, меняем только текст builder.row( InlineKeyboardButton( text="ℹ️ О нас", - url="https://www.youtube.com/watch?v=Zirn-CKck-c" + url="https://www.youtube.com/watch?v=Zirn-CKck-c", ) ) return builder.as_markup() @@ -450,7 +471,7 @@ def tarif_confirm_keyboard(name, amount, classif): def confirm_popup_keyboard(): - """ + """ аааааааааааааааааааааа """ diff --git a/main.py b/main.py index 04735c9..1b3e2b4 100644 --- a/main.py +++ b/main.py @@ -11,7 +11,8 @@ import logging BOT_TOKEN = os.getenv("TOKEN") if not BOT_TOKEN: - raise ValueError("Не задан токен бота. Убедитесь, что переменная окружения 'TOKEN' установлена.") + raise ValueError( + "Не задан токен бота. Убедитесь, что переменная окружения 'TOKEN' установлена.") bot = Bot(token=BOT_TOKEN) dp = Dispatcher() @@ -23,9 +24,10 @@ dp.message.middleware(AntiSpamMiddleware(rate_limit=1)) async def set_commands(): """Устанавливает команды для бота.""" commands = [ - BotCommand(command="/start", description="🥚Главное меню"), - BotCommand(command="/subscriptions", description="🦴Мои подписки"), - BotCommand(command="/support", description="❕Поддержка❕"), + BotCommand(command="start", description="Запуск бота"), + BotCommand(command="subscriptions", description="Мои подписки"), + BotCommand(command="support", description="Техподдержка"), + BotCommand(command="referrals", description="Реферальная программа"), ] await bot.set_my_commands(commands) From d1417653c3572cdf9f91951a7cb424ab26a90496 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 25 Nov 2025 22:40:42 +0300 Subject: [PATCH 08/13] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20keyboards:=20?= =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20account/popup/?= =?UTF-8?q?payment=5Fmethods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyboard/keyboards.py | 308 +++++++++++++++++++----------------------- 1 file changed, 137 insertions(+), 171 deletions(-) diff --git a/keyboard/keyboards.py b/keyboard/keyboards.py index b3037c2..8e80012 100644 --- a/keyboard/keyboards.py +++ b/keyboard/keyboards.py @@ -1,18 +1,10 @@ -from aiogram.types import InlineKeyboardButton -from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder from aiogram.types import InlineKeyboardButton, KeyboardButton -from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder -from aiogram.types import InlineKeyboardButton, KeyboardButton - -# ... остальной код выше не трогаем ... - - def main_keyboard(): """ - Главное меню (только визуал перетянут под твой стиль) + Главное меню """ builder = InlineKeyboardBuilder() builder.row( @@ -33,7 +25,6 @@ def main_keyboard(): callback_data="faq", ) ) - # Оставляем URL как у Вовы, меняем только текст builder.row( InlineKeyboardButton( text="ℹ️ О нас", @@ -43,7 +34,31 @@ def main_keyboard(): return builder.as_markup() +def account_keyboard(): + """ + Клавиатура профиля: + только пополнить баланс и история транзакций. + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="🪙 Пополнить баланс", + callback_data="balance", + ) + ) + builder.row( + InlineKeyboardButton( + text="🧾 История транзакций", + callback_data="tranhist", + ) + ) + return builder.as_markup() + + def balance_keyboard(): + """ + Экран баланса + """ builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( @@ -66,6 +81,64 @@ def balance_keyboard(): return builder.as_markup() +def popup_keyboard(): + """ + Суммы пополнения. + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="200 ₽", callback_data="popup:200"), + InlineKeyboardButton(text="500 ₽", callback_data="popup:500"), + ) + builder.row( + InlineKeyboardButton(text="1000 ₽", callback_data="popup:1000"), + InlineKeyboardButton(text="2000 ₽", callback_data="popup:2000"), + ) + builder.row( + InlineKeyboardButton(text="3000 ₽", callback_data="popup:3000"), + InlineKeyboardButton(text="5000 ₽", callback_data="popup:5000"), + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="balance", + ) + ) + return builder.as_markup() + + +def payment_methods_keyboard(amount: int): + """ + Способы оплаты для выбранной суммы. + """ + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="⭐ Telegram Stars", + callback_data=f"method_stars_{amount}", + ) + ) + builder.row( + InlineKeyboardButton( + text="💵 YooKassa", + callback_data=f"method_ykassa_{amount}", + ) + ) + builder.row( + InlineKeyboardButton( + text="🪙 CryptoBot", + callback_data=f"method_crypto_{amount}", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="popup", + ) + ) + return builder.as_markup() + + def ticket_list_keyboard(tickets): builder = InlineKeyboardBuilder() for ticket in tickets: @@ -145,164 +218,8 @@ def buy_keyboard(): def tarif_Lark_keyboard(): - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🐣 Lark 1 месяц", - callback_data="Lark:Standart:1", - ) - ) - builder.row( - InlineKeyboardButton( - text="🐣 Lark 6 месяцев", - callback_data="Lark:Standart:6", - ) - ) - builder.row( - InlineKeyboardButton( - text="🐣 Lark 12 месяцев", - callback_data="Lark:Standart:12", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="buy_subscription", - ) - ) - return builder.as_markup() - - -def tarif_Lark_pro_keyboard(): - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🦅 Lark Pro 1 месяц", - callback_data="Lark:Pro:1", - ) - ) - builder.row( - InlineKeyboardButton( - text="🦅 Lark Pro 6 месяцев", - callback_data="Lark:Pro:6", - ) - ) - builder.row( - InlineKeyboardButton( - text="🦅 Lark Pro 12 месяцев", - callback_data="Lark:Pro:12", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="buy_subscription", - ) - ) - return builder.as_markup() - - -def tarif_Lark_family_keyboard(): - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="👨‍👩‍👧 Lark Family 1 месяц", - callback_data="Lark:Family:1", - ) - ) - builder.row( - InlineKeyboardButton( - text="👨‍👩‍👧 Lark Family 6 месяцев", - callback_data="Lark:Family:6", - ) - ) - builder.row( - InlineKeyboardButton( - text="👨‍👩‍👧 Lark Family 12 месяцев", - callback_data="Lark:Family:12", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="buy_subscription", - ) - ) - return builder.as_markup() - - -def subscriptions_card_keyboard(sub_id: str, index: int, total: int): """ - Карточка подписки: - - навигация ⬅️/➡️ - - 'Конфиг' / 'Продлить' - - 'Новая' / 'Назад' - """ - builder = InlineKeyboardBuilder() - - nav = [] - if index > 0: - nav.append( - InlineKeyboardButton( - text="⬅️", - callback_data=f"sub_prev:{index-1}", - ) - ) - if index < total - 1: - nav.append( - InlineKeyboardButton( - text="➡️", - callback_data=f"sub_next:{index+1}", - ) - ) - if nav: - builder.row(*nav) - - builder.row( - InlineKeyboardButton( - text="🔑 Конфиг", - callback_data=f"sub_cfg:{sub_id}", - ), - InlineKeyboardButton( - text="🔁 Продлить", - callback_data=f"sub_renew:{sub_id}", - ), - ) - builder.row( - InlineKeyboardButton( - text="➕ Новая", - callback_data="buy_subscription", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data="profile", - ) - ) - return builder.as_markup() - - -def tarif_confirm_keyboard(name: str, amount: int, classif: str): - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="✅ Подтвердить", - callback_data=f"confirm:{name}_{classif}_{amount}", - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Отменить", - callback_data="buy_subscription", - ) - ) - return builder.as_markup() - - -def tarif_Lark_keyboard(): - """ - Тариф Lark (Standart) — только подписи меняем. + Тариф Lark Basic (Standart) """ builder = InlineKeyboardBuilder() builder.row( @@ -396,6 +313,56 @@ def tarif_Lark_family_keyboard(): return builder.as_markup() +def subscriptions_card_keyboard(sub_id: str, index: int, total: int): + """ + Карточка подписки: + навигация, конфиг, продление, новая, назад. + """ + builder = InlineKeyboardBuilder() + + nav = [] + if index > 0: + nav.append( + InlineKeyboardButton( + text="⬅️", + callback_data=f"sub_prev:{index-1}", + ) + ) + if index < total - 1: + nav.append( + InlineKeyboardButton( + text="➡️", + callback_data=f"sub_next:{index+1}", + ) + ) + if nav: + builder.row(*nav) + + builder.row( + InlineKeyboardButton( + text="🔑 Конфиг", + callback_data=f"sub_cfg:{sub_id}", + ), + InlineKeyboardButton( + text="🔁 Продлить", + callback_data=f"sub_renew:{sub_id}", + ), + ) + builder.row( + InlineKeyboardButton( + text="➕ Новая", + callback_data="buy_subscription", + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", + callback_data="profile", + ) + ) + return builder.as_markup() + + def guide_keyboard(): """ Руководство по подключению @@ -450,7 +417,7 @@ def tranhist_keyboard(): return builder.as_markup() -def tarif_confirm_keyboard(name, amount, classif): +def tarif_confirm_keyboard(name: str, amount: int, classif: str): """ Подтверждение покупки тарифа """ @@ -471,9 +438,8 @@ def tarif_confirm_keyboard(name, amount, classif): def confirm_popup_keyboard(): - """ - аааааааааааааааааааааа - + """ + Подтверждение пополнения. """ builder = InlineKeyboardBuilder() builder.row( From 77ca27a09c771eff0cc5d52441bcd618760475ed Mon Sep 17 00:00:00 2001 From: root Date: Sun, 30 Nov 2025 16:33:04 +0300 Subject: [PATCH 09/13] =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BE=D0=BF=D0=BB=D0=B0=D1=82=D1=83=20=D1=8E?= =?UTF-8?q?=D0=BA=D0=B0=D1=81=D1=81=D1=8B=20=D0=B8=20=D0=BA=D1=80=D0=B8?= =?UTF-8?q?=D0=BF=D1=82=D0=BE=D0=B1=D0=BE=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/profile.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/handlers/profile.py b/handlers/profile.py index bd10299..79fe1a1 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -247,13 +247,25 @@ async def method_stars_handler(callback: CallbackQuery): @router.callback_query(lambda callback: callback.data.startswith("method_ykassa_")) async def method_ykassa_handler(callback: CallbackQuery): """ - Заглушка: оплата через YooKassa. + Обработчик оплаты через YooKassa. """ amount = callback.data.split("_")[-1] + data = {"user_id": callback.from_user.id, "amount": amount, "provider": "yookassa"} + result = await call_api("POST", "billing/payments/init", data) + + payment_url = result.get("confirmation_url", "#") + payment_id = result.get("payment_id", "") + await callback.message.edit_text( - f"💵 Оплата через YooKassa на {amount} ₽ пока в разработке.\n\n" - "Функционал появится после настройки биллинга.", + f"💵 Оплата через YooKassa\n\n" + f"💰 Сумма: {amount} руб\n" + f"📋 ID платежа: {payment_id}\n\n" + f"➡️ Перейти к оплате\n\n" + f"После оплаты нажмите кнопку 'Проверить оплату'", + parse_mode="HTML", + disable_web_page_preview=True ) + await callback.answer() @@ -263,8 +275,23 @@ async def method_crypto_handler(callback: CallbackQuery): Заглушка: оплата через CryptoBot. """ amount = callback.data.split("_")[-1] + data = {"user_id": callback.from_user.id, "amount": amount, "provider": "cryptobot"} + result = await call_api("POST", "billing/payments/init", data) + + payment_url = result.get("confirmation_url", "#") + payment_id = result.get("payment_id", "") + await callback.message.edit_text( - f"🪙 Оплата через CryptoBot на {amount} ₽ пока в разработке.\n\n" + f"💵 🪙 Оплата через CryptoBot\n\n" + f"💰 Сумма: {amount} руб\n" + f"📋 ID платежа: {payment_id}\n\n" + f"➡️ Перейти к оплате\n\n" + f"После оплаты нажмите кнопку 'Проверить оплату'", + parse_mode="HTML", + disable_web_page_preview=True + ) + await callback.message.edit_text( + f" {amount} ₽ пока в разработке.\n\n" "Платёжный шлюз будет добавлен позже.", ) await callback.answer() From 3e976d996007a069ae82377cc32579cac0ec5266 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 30 Nov 2025 16:41:40 +0300 Subject: [PATCH 10/13] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83=20=D1=81=20?= =?UTF-8?q?id=20=D0=B8=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B8=D0=BB=20call?= =?UTF-8?q?=5Fapi=20=D1=87=D1=82=D0=BE=20=D0=B1=D1=8B=20=D0=BE=D0=BD=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=BB=20=D1=81=20billing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/profile.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/handlers/profile.py b/handlers/profile.py index 79fe1a1..ce70e17 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -24,11 +24,11 @@ router = Router() logger = logging.getLogger(__name__) -async def call_api(method, endpoint, data=None): +async def call_api(method, endpoint, data=None,base_url = BASE_URL_FASTAPI): """ Выполняет HTTP-запрос к FastAPI. """ - url = f"{BASE_URL_FASTAPI}{endpoint}" + url = f"{base_url}{endpoint}" logger.info(f"Инициализация запроса: {method} {url} с данными {data}") try: @@ -71,7 +71,7 @@ async def profile_callback_handler(callback: CallbackQuery): # Последняя подписка пользователя sub_data = await call_api( - "GET", f"/subscription/{user_data['id']}/last" + "GET", f"/subscription/{user_data['telegram_id']}/last" ) if sub_data == "ERROR" or not isinstance(sub_data, dict): sub_data = None @@ -171,7 +171,7 @@ async def tranhist_callback_handler(callback: CallbackQuery): try: transactions = await call_api( - "GET", f"/user/{user_data['id']}/transactions" + "GET", f"/user/{user_data['telegram_id']}/transactions" ) if not transactions: await callback.message.edit_text( @@ -251,7 +251,7 @@ async def method_ykassa_handler(callback: CallbackQuery): """ amount = callback.data.split("_")[-1] data = {"user_id": callback.from_user.id, "amount": amount, "provider": "yookassa"} - result = await call_api("POST", "billing/payments/init", data) + result = await call_api("POST", "billing/payments/init", data,"http://billing:8000") payment_url = result.get("confirmation_url", "#") payment_id = result.get("payment_id", "") @@ -276,7 +276,7 @@ async def method_crypto_handler(callback: CallbackQuery): """ amount = callback.data.split("_")[-1] data = {"user_id": callback.from_user.id, "amount": amount, "provider": "cryptobot"} - result = await call_api("POST", "billing/payments/init", data) + result = await call_api("POST", "billing/payments/init", data,"http://billing:8000") payment_url = result.get("confirmation_url", "#") payment_id = result.get("payment_id", "") From 6276490ee11961ffa232af1f78b63808155ea9a6 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 30 Nov 2025 16:51:47 +0300 Subject: [PATCH 11/13] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=20call=5Fapi=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=B2=20=D1=82=D0=B8=D0=BF=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D0=B8=20=D0=BF=D1=80=D0=B5=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B7=D1=83=D1=8E=20=D0=B2=D0=BE=20float=20amount?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/profile.py | 49 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/handlers/profile.py b/handlers/profile.py index ce70e17..e99d32c 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -33,7 +33,12 @@ async def call_api(method, endpoint, data=None,base_url = BASE_URL_FASTAPI): try: async with aiohttp.ClientSession() as session: - async with session.request(method, url, json=data) as response: + async with session.request( + method, + url, + json=data, + headers={"Content-Type": "application/json"} + ) as response: logger.info( f"Получен ответ от {url}: статус {response.status}" ) @@ -250,8 +255,34 @@ async def method_ykassa_handler(callback: CallbackQuery): Обработчик оплаты через YooKassa. """ amount = callback.data.split("_")[-1] - data = {"user_id": callback.from_user.id, "amount": amount, "provider": "yookassa"} - result = await call_api("POST", "billing/payments/init", data,"http://billing:8000") + + # Формируем данные для отправки + data = { + "user_id": callback.from_user.id, + "amount": float(amount), # Преобразуем в float + "provider": "yookassa" + } + + logger.info(f"Отправка запроса на инициализацию платежа: {data}") + + # Отправляем POST запрос с JSON телом + result = await call_api("POST", "/billing/payments/init", data, "http://billing:8000") + + if result == "ERROR": + await callback.message.edit_text( + "❌ Произошла ошибка при создании платежа. Попробуйте позже." + ) + await callback.answer() + return + + # Проверяем успешность операции + if not result.get("success", False): + error_msg = result.get("error", "Неизвестная ошибка") + await callback.message.edit_text( + f"❌ Ошибка: {error_msg}" + ) + await callback.answer() + return payment_url = result.get("confirmation_url", "#") payment_id = result.get("payment_id", "") @@ -263,7 +294,15 @@ async def method_ykassa_handler(callback: CallbackQuery): f"➡️ Перейти к оплате\n\n" f"После оплаты нажмите кнопку 'Проверить оплату'", parse_mode="HTML", - disable_web_page_preview=True + disable_web_page_preview=True, + reply_markup=types.InlineKeyboardMarkup( + inline_keyboard=[[ + types.InlineKeyboardButton( + text="🔄 Проверить оплату", + callback_data=f"check_payment:{payment_id}" + )] + ] + ) ) await callback.answer() @@ -276,7 +315,7 @@ async def method_crypto_handler(callback: CallbackQuery): """ amount = callback.data.split("_")[-1] data = {"user_id": callback.from_user.id, "amount": amount, "provider": "cryptobot"} - result = await call_api("POST", "billing/payments/init", data,"http://billing:8000") + result = await call_api("POST", "billing/payments/init", data,"http://billing:8000/") payment_url = result.get("confirmation_url", "#") payment_id = result.get("payment_id", "") From 12c60b884a1c6943001ff3cbfeb8225b1177e11f Mon Sep 17 00:00:00 2001 From: root Date: Sun, 30 Nov 2025 16:57:08 +0300 Subject: [PATCH 12/13] =?UTF-8?q?=D0=9E=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B2=20=D1=81=D0=BF=D0=BE=D1=81=D0=BE=D0=B1=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D1=87=D0=B8=20=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85,=20=D0=BD=D1=83=D0=B6=D0=B5=D0=BD=20=D0=B1?= =?UTF-8?q?=D1=8B=D0=BB=20query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/profile.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/handlers/profile.py b/handlers/profile.py index e99d32c..3c9145e 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -256,17 +256,13 @@ async def method_ykassa_handler(callback: CallbackQuery): """ amount = callback.data.split("_")[-1] - # Формируем данные для отправки - data = { - "user_id": callback.from_user.id, - "amount": float(amount), # Преобразуем в float - "provider": "yookassa" - } + # Формируем URL с query parameters вместо JSON body + endpoint = f"/billing/payments/init?user_id={callback.from_user.id}&amount={float(amount)}&provider=yookassa" - logger.info(f"Отправка запроса на инициализацию платежа: {data}") + logger.info(f"Отправка запроса на инициализацию платежа: {endpoint}") - # Отправляем POST запрос с JSON телом - result = await call_api("POST", "/billing/payments/init", data, "http://billing:8000") + # Отправляем POST запрос с пустым телом (параметры в URL) + result = await call_api("POST", endpoint, None, "http://billing:8000") if result == "ERROR": await callback.message.edit_text( From 0d9a86ee543107b90313b5e5bb6e065d31537f8a Mon Sep 17 00:00:00 2001 From: root Date: Sun, 30 Nov 2025 18:13:00 +0300 Subject: [PATCH 13/13] =?UTF-8?q?=D0=94=D0=BB=D1=8F=20=D0=BA=D1=80=D0=B8?= =?UTF-8?q?=D0=BF=D1=82=D0=BE=D0=B1=D0=BE=D1=82=D0=B0=20=D1=82=D0=B0=D0=BA?= =?UTF-8?q?=20=D0=B6=D0=B5=20=D0=BF=D0=BE=D1=84=D0=B8=D0=BA=D1=81=D0=B8?= =?UTF-8?q?=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/profile.py | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/handlers/profile.py b/handlers/profile.py index 3c9145e..94632c2 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -310,25 +310,50 @@ async def method_crypto_handler(callback: CallbackQuery): Заглушка: оплата через CryptoBot. """ amount = callback.data.split("_")[-1] - data = {"user_id": callback.from_user.id, "amount": amount, "provider": "cryptobot"} - result = await call_api("POST", "billing/payments/init", data,"http://billing:8000/") + endpoint = f"/billing/payments/init?user_id={callback.from_user.id}&amount={float(amount)}&provider=cryptobot" + + logger.info(f"Отправка запроса на инициализацию платежа: {endpoint}") + + # Отправляем POST запрос с пустым телом (параметры в URL) + result = await call_api("POST", endpoint, None, "http://billing:8000") + + if result == "ERROR": + await callback.message.edit_text( + "❌ Произошла ошибка при создании платежа. Попробуйте позже." + ) + await callback.answer() + return + + # Проверяем успешность операции + if not result.get("success", False): + error_msg = result.get("error", "Неизвестная ошибка") + await callback.message.edit_text( + f"❌ Ошибка: {error_msg}" + ) + await callback.answer() + return payment_url = result.get("confirmation_url", "#") payment_id = result.get("payment_id", "") - + await callback.message.edit_text( - f"💵 🪙 Оплата через CryptoBot\n\n" + f"💵 Оплата через Сryptobot\n\n" f"💰 Сумма: {amount} руб\n" f"📋 ID платежа: {payment_id}\n\n" f"➡️ Перейти к оплате\n\n" f"После оплаты нажмите кнопку 'Проверить оплату'", parse_mode="HTML", - disable_web_page_preview=True - ) - await callback.message.edit_text( - f" {amount} ₽ пока в разработке.\n\n" - "Платёжный шлюз будет добавлен позже.", + disable_web_page_preview=True, + reply_markup=types.InlineKeyboardMarkup( + inline_keyboard=[[ + types.InlineKeyboardButton( + text="🔄 Проверить оплату", + callback_data=f"check_payment:{payment_id}" + )] + ] + ) ) + await callback.answer()