Merge pull request #41 from yazhog/main

Убрал копейки + QR код для рефералки
This commit is contained in:
Egor
2025-09-07 18:26:46 +03:00
committed by GitHub
14 changed files with 134 additions and 63 deletions

View File

@@ -179,8 +179,8 @@ class Settings(BaseSettings):
return ["ru", "en"]
def format_price(self, price_kopeks: int) -> str:
rubles = price_kopeks / 100
return f"{rubles:.2f}"
rubles = price_kopeks // 100
return f"{rubles}"
def kopeks_to_rubles(self, kopeks: int) -> float:
return kopeks / 100

View File

@@ -45,8 +45,8 @@ class TelegramStarsService:
)
logger.info(
f"Создан Stars invoice на {stars_amount} звезд (~{amount_rubles:.2f}₽) "
f"для {chat_id}, курс: {settings.get_stars_rate():.2f}₽/⭐"
f"Создан Stars invoice на {stars_amount} звезд (~{int(amount_rubles)}₽) "
f"для {chat_id}, курс: {int(settings.get_stars_rate())}₽/⭐"
)
return invoice_link
@@ -80,7 +80,7 @@ class TelegramStarsService:
logger.info(
f"Отправлен Stars invoice {message.message_id} на {stars_amount} звезд "
f"(~{amount_rubles:.2f}₽), курс: {settings.get_stars_rate():.2f}₽/⭐"
f"(~{int(amount_rubles)}₽), курс: {int(settings.get_stars_rate())}₽/⭐"
)
return {
"message_id": message.message_id,

View File

@@ -37,7 +37,7 @@ async def show_servers_menu(
С подключениями: {stats['servers_with_connections']}
💰 <b>Выручка от серверов:</b>
• Общая: {stats['total_revenue_rubles']:.2f}
• Общая: {int(stats['total_revenue_rubles'])}
Выберите действие:
"""
@@ -83,7 +83,7 @@ async def show_servers_list(
for i, server in enumerate(servers, 1 + (page - 1) * 10):
status_emoji = "" if server.is_available else ""
price_text = f"{server.price_rubles:.2f}" if server.price_kopeks > 0 else "Бесплатно"
price_text = f"{int(server.price_rubles)}" if server.price_kopeks > 0 else "Бесплатно"
text += f"{i}. {status_emoji} {server.display_name}\n"
text += f" 💰 Цена: {price_text}"
@@ -222,7 +222,7 @@ async def show_server_edit_menu(
return
status_emoji = "✅ Доступен" if server.is_available else "❌ Недоступен"
price_text = f"{server.price_rubles:.2f}" if server.price_kopeks > 0 else "Бесплатно"
price_text = f"{int(server.price_rubles)}" if server.price_kopeks > 0 else "Бесплатно"
text = f"""
🌐 <b>Редактирование сервера</b>
@@ -304,7 +304,7 @@ async def toggle_server_availability(
server = await get_server_squad_by_id(db, server_id)
status_emoji = "✅ Доступен" if server.is_available else "❌ Недоступен"
price_text = f"{server.price_rubles:.2f}" if server.price_kopeks > 0 else "Бесплатно"
price_text = f"{int(server.price_rubles)}" if server.price_kopeks > 0 else "Бесплатно"
text = f"""
🌐 <b>Редактирование сервера</b>
@@ -378,7 +378,7 @@ async def start_server_edit_price(
await state.set_data({'server_id': server_id})
await state.set_state(AdminStates.editing_server_price)
current_price = f"{server.price_rubles:.2f}" if server.price_kopeks > 0 else "Бесплатно"
current_price = f"{int(server.price_rubles)}" if server.price_kopeks > 0 else "Бесплатно"
await callback.message.edit_text(
f"💰 <b>Редактирование цены</b>\n\n"
@@ -424,7 +424,7 @@ async def process_server_price_edit(
await cache.delete("available_countries")
price_text = f"{price_rubles:.2f}" if price_kopeks > 0 else "Бесплатно"
price_text = f"{int(price_rubles)}" if price_kopeks > 0 else "Бесплатно"
await message.answer(
f"✅ Цена сервера изменена на: <b>{price_text}</b>",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
@@ -613,8 +613,8 @@ async def show_server_detailed_stats(
С активными подключениями: {stats['servers_with_connections']}
<b>💰 Финансовая статистика:</b>
• Общая выручка: {stats['total_revenue_rubles']:.2f}
• Средняя цена за сервер: {(stats['total_revenue_rubles'] / max(stats['servers_with_connections'], 1)):.2f}
• Общая выручка: {int(stats['total_revenue_rubles'])}
• Средняя цена за сервер: {int(stats['total_revenue_rubles'] / max(stats['servers_with_connections'], 1))}
<b>🔥 Топ серверов по цене:</b>
"""
@@ -622,7 +622,7 @@ async def show_server_detailed_stats(
sorted_servers = sorted(available_servers, key=lambda x: x.price_kopeks, reverse=True)
for i, server in enumerate(sorted_servers[:5], 1):
price_text = f"{server.price_rubles:.2f}" if server.price_kopeks > 0 else "Бесплатно"
price_text = f"{int(server.price_rubles)}" if server.price_kopeks > 0 else "Бесплатно"
text += f"{i}. {server.display_name} - {price_text}\n"
if not sorted_servers:

View File

@@ -636,9 +636,9 @@ async def process_balance_edit(
description = f"Изменение баланса администратором {db_user.full_name}"
if amount_kopeks > 0:
description = f"Пополнение администратором: +{amount_rubles}"
description = f"Пополнение администратором: +{int(amount_rubles)}"
else:
description = f"Списание администратором: {amount_rubles}"
description = f"Списание администратором: {int(amount_rubles)}"
success = await user_service.update_user_balance(
db, user_id, amount_kopeks, description, db_user.id

View File

@@ -381,7 +381,7 @@ async def process_stars_payment_amount(
f"⭐ <b>Оплата через Telegram Stars</b>\n\n"
f"💰 Сумма: {texts.format_price(amount_kopeks)}\n"
f"К оплате: {stars_amount} звезд\n"
f"📊 Курс: {settings.get_stars_rate():.2f}₽ за звезду\n\n"
f"📊 Курс: {int(settings.get_stars_rate())}₽ за звезду\n\n"
f"Нажмите кнопку ниже для оплаты:",
reply_markup=keyboard,
parse_mode="HTML"
@@ -465,7 +465,7 @@ async def process_yookassa_payment_amount(
await state.clear()
logger.info(f"Создан платеж YooKassa для пользователя {db_user.telegram_id}: "
f"{amount_kopeks/100}₽, ID: {payment_result['yookassa_payment_id']}")
f"{amount_kopeks//100}₽, ID: {payment_result['yookassa_payment_id']}")
except Exception as e:
logger.error(f"Ошибка создания YooKassa платежа: {e}")

View File

@@ -1,12 +1,21 @@
import logging
from aiogram import Dispatcher, types, F
from pathlib import Path
import qrcode
from aiogram import Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest
from aiogram.types import FSInputFile
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import User
from app.keyboards.inline import get_referral_keyboard, get_back_keyboard
from app.keyboards.inline import get_referral_keyboard
from app.localization.texts import get_texts
from app.utils.user_utils import get_user_referral_summary, get_detailed_referral_list, get_referral_analytics
from app.utils.user_utils import (
get_detailed_referral_list,
get_referral_analytics,
get_user_referral_summary,
)
logger = logging.getLogger(__name__)
@@ -82,11 +91,59 @@ async def show_referral_info(
referral_text += "📢 Приглашайте друзей и зарабатывайте!"
await callback.message.edit_text(
referral_text,
reply_markup=get_referral_keyboard(db_user.language),
parse_mode="HTML"
if callback.message.text:
await callback.message.edit_text(
referral_text,
reply_markup=get_referral_keyboard(db_user.language),
parse_mode="HTML"
)
else:
await callback.message.delete()
await callback.message.answer(
referral_text,
reply_markup=get_referral_keyboard(db_user.language),
parse_mode="HTML"
)
await callback.answer()
async def show_referral_qr(
callback: types.CallbackQuery,
db_user: User,
):
texts = get_texts(db_user.language)
bot_username = (await callback.bot.get_me()).username
referral_link = f"https://t.me/{bot_username}?start={db_user.referral_code}"
qr_dir = Path("data") / "referral_qr"
qr_dir.mkdir(parents=True, exist_ok=True)
file_path = qr_dir / f"{db_user.id}.png"
if not file_path.exists():
img = qrcode.make(referral_link)
img.save(file_path)
photo = FSInputFile(file_path)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_referrals")]]
)
try:
await callback.message.edit_media(
types.InputMediaPhoto(
media=photo,
caption=f"🔗 Ваша реферальная ссылка:\n{referral_link}",
),
reply_markup=keyboard,
)
except TelegramBadRequest:
await callback.message.delete()
await callback.message.answer_photo(
photo,
caption=f"🔗 Ваша реферальная ссылка:\n{referral_link}",
reply_markup=keyboard,
)
await callback.answer()
@@ -243,6 +300,11 @@ def register_handlers(dp: Dispatcher):
create_invite_message,
F.data == "referral_create_invite"
)
dp.callback_query.register(
show_referral_qr,
F.data == "referral_show_qr"
)
dp.callback_query.register(
show_detailed_referral_list,

View File

@@ -92,7 +92,7 @@ async def handle_successful_payment(
await message.answer(
f"🎉 <b>Платеж успешно обработан!</b>\n\n"
f"⭐ Потрачено звезд: {payment.total_amount}\n"
f"💰 Зачислено на баланс: {rubles_amount:.2f}\n"
f"💰 Зачислено на баланс: {int(rubles_amount)}\n"
f"🆔 ID транзакции: {payment.telegram_payment_charge_id[:8]}...\n\n"
f"Спасибо за пополнение! 🚀",
parse_mode="HTML"
@@ -100,7 +100,7 @@ async def handle_successful_payment(
logger.info(
f"✅ Stars платеж успешно обработан: "
f"пользователь {user.id}, {payment.total_amount} звезд → {rubles_amount:.2f}"
f"пользователь {user.id}, {payment.total_amount} звезд → {int(rubles_amount)}"
)
else:
logger.error(f"Ошибка обработки Stars платежа для пользователя {user.id}")

View File

@@ -521,10 +521,10 @@ async def complete_registration_from_callback(
except Exception as e:
logger.error(f"Ошибка при отправке главного меню: {e}")
try:
balance_rubles = user.balance_kopeks / 100
balance_rubles = user.balance_kopeks // 100
await callback.message.answer(
f"Добро пожаловать, {user.full_name}!\n"
f"Баланс: {balance_rubles:.2f}\n"
f"Баланс: {balance_rubles}\n"
f"Подписка: Нет активной подписки",
reply_markup=get_main_menu_keyboard(
language=user.language,
@@ -684,10 +684,10 @@ async def complete_registration(
except Exception as e:
logger.error(f"Ошибка при отправке главного меню: {e}")
try:
balance_rubles = user.balance_kopeks / 100
balance_rubles = user.balance_kopeks // 100
await message.answer(
f"Добро пожаловать, {user.full_name}!\n"
f"Баланс: {balance_rubles:.2f}\n"
f"Баланс: {balance_rubles}\n"
f"Подписка: Нет активной подписки",
reply_markup=get_main_menu_keyboard(
language=user.language,

View File

@@ -1415,13 +1415,13 @@ async def get_traffic_packages_info() -> str:
info_lines.append("\n✅ Активные:")
for pkg in enabled_packages:
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
info_lines.append(f"{gb_text}: {pkg['price']/100}")
info_lines.append(f"{gb_text}: {pkg['price']//100}")
if disabled_packages:
info_lines.append("\n❌ Отключенные:")
for pkg in disabled_packages:
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
info_lines.append(f"{gb_text}: {pkg['price']/100}")
info_lines.append(f"{gb_text}: {pkg['price']//100}")
info_lines.append(f"\n📊 Всего пакетов: {len(packages)}")
info_lines.append(f"🟢 Активных: {len(enabled_packages)}")
@@ -1918,9 +1918,9 @@ async def confirm_purchase(
if remnawave_user and hasattr(subscription, 'subscription_url') and subscription.subscription_url:
success_text = f"{texts.SUBSCRIPTION_PURCHASED}\n\n"
success_text += f"Ваша ссылка для импорта в VPN приложение:\n"
success_text += f"🔗 <b>Ваша ссылка для импорта в VPN приложение:</b>\n"
success_text += f"<code>{subscription.subscription_url}</code>\n\n"
success_text += f"Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве"
success_text += f"📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве"
connect_mode = settings.CONNECT_BUTTON_MODE
@@ -1928,12 +1928,12 @@ async def confirm_purchase(
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text="Подключиться",
text="🔗 Подключиться",
web_app=types.WebAppInfo(url=subscription.subscription_url),
)
],
[InlineKeyboardButton(text="Моя подписка", callback_data="menu_subscription")],
[InlineKeyboardButton(text="В главное меню", callback_data="back_to_menu")],
[InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")],
[InlineKeyboardButton(text="⬅️ В главное меню", callback_data="back_to_menu")],
])
elif connect_mode == "miniapp_custom":
if not settings.MINIAPP_CUSTOM_URL:
@@ -1943,18 +1943,18 @@ async def confirm_purchase(
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text="Подключиться",
text="🔗 Подключиться",
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL),
)
],
[InlineKeyboardButton(text="Моя подписка", callback_data="menu_subscription")],
[InlineKeyboardButton(text="В главное меню", callback_data="back_to_menu")],
[InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")],
[InlineKeyboardButton(text="⬅️ В главное меню", callback_data="back_to_menu")],
])
else:
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Подключиться", callback_data="subscription_connect")],
[InlineKeyboardButton(text="Моя подписка", callback_data="menu_subscription")],
[InlineKeyboardButton(text="В главное меню", callback_data="back_to_menu")],
[InlineKeyboardButton(text="🔗 Подключиться", callback_data="subscription_connect")],
[InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")],
[InlineKeyboardButton(text="⬅️ В главное меню", callback_data="back_to_menu")],
])
await callback.message.edit_text(

View File

@@ -533,6 +533,12 @@ def get_referral_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
callback_data="referral_create_invite"
)
],
[
InlineKeyboardButton(
text="📱 Показать QR код",
callback_data="referral_show_qr"
)
],
[
InlineKeyboardButton(
text="👥 Список рефералов",
@@ -715,14 +721,14 @@ def get_add_traffic_keyboard(language: str = "ru", subscription_end_date: dateti
if gb == 0:
if language == "ru":
text = f"♾️ Безлимитный трафик - {total_price/100:.2f}{period_text}"
text = f"♾️ Безлимитный трафик - {total_price//100}{period_text}"
else:
text = f"♾️ Unlimited traffic - {total_price/100:.2f}{period_text}"
text = f"♾️ Unlimited traffic - {total_price//100}{period_text}"
else:
if language == "ru":
text = f"📊 +{gb} ГБ трафика - {total_price/100:.2f}{period_text}"
text = f"📊 +{gb} ГБ трафика - {total_price//100}{period_text}"
else:
text = f"📊 +{gb} GB traffic - {total_price/100:.2f}{period_text}"
text = f"📊 +{gb} GB traffic - {total_price//100}{period_text}"
buttons.append([
InlineKeyboardButton(text=text, callback_data=f"add_traffic_{gb}")
@@ -761,9 +767,9 @@ def get_add_devices_keyboard(current_devices: int, language: str = "ru", subscri
total_price = price_per_month * months_multiplier
if language == "ru":
text = f"📱 +{count} устройство(а) (итого: {new_total}) - {total_price/100:.2f}{period_text}"
text = f"📱 +{count} устройство(а) (итого: {new_total}) - {total_price//100}{period_text}"
else:
text = f"📱 +{count} device(s) (total: {new_total}) - {total_price/100:.2f}{period_text}"
text = f"📱 +{count} device(s) (total: {new_total}) - {total_price//100}{period_text}"
buttons.append([
InlineKeyboardButton(text=text, callback_data=f"add_devices_{count}")
@@ -835,10 +841,10 @@ def get_manage_countries_keyboard(
if uuid not in current_subscription_countries and uuid in selected:
total_price = price_per_month * months_multiplier
if months_multiplier > 1:
price_text = f" ({price_per_month/100:.2f}₽/мес × {months_multiplier} = {total_price/100:.2f}₽)"
price_text = f" ({price_per_month//100}₽/мес × {months_multiplier} = {total_price//100}₽)"
logger.info(f"🔍 Сервер {name}: {price_per_month/100}₽/мес × {months_multiplier} мес = {total_price/100}")
else:
price_text = f" ({total_price/100:.2f}₽)"
price_text = f" ({total_price//100}₽)"
display_name = f"{icon} {name}{price_text}"
else:
display_name = f"{icon} {name}"
@@ -851,7 +857,7 @@ def get_manage_countries_keyboard(
])
if total_cost > 0:
apply_text = f"✅ Применить изменения ({total_cost/100:.2f} ₽)"
apply_text = f"✅ Применить изменения ({total_cost//100} ₽)"
logger.info(f"🔍 Общая стоимость новых серверов: {total_cost/100}")
else:
apply_text = "✅ Применить изменения"

View File

@@ -57,7 +57,7 @@ class Texts:
@staticmethod
def format_price(kopeks: int) -> str:
return f"{kopeks / 100:.2f}"
return f"{int(kopeks / 100)}"
@staticmethod
def format_traffic(gb: float) -> str:
@@ -125,7 +125,7 @@ class RussianTexts(Texts):
MENU_LANGUAGE = "🌐 Язык"
MENU_ADMIN = "⚙️ Админ-панель"
BALANCE_BUTTON = "💰 Баланс: {balance}"
BALANCE_BUTTON_ZERO = "💰 Баланс: 0.00"
BALANCE_BUTTON_ZERO = "💰 Баланс: 0 ₽"
SUBSCRIPTION_NONE = "❌ Нет активной подписки"
SUBSCRIPTION_TRIAL = "🧪 Тестовая подписка"

View File

@@ -48,7 +48,7 @@ class PaymentService:
prices=[LabeledPrice(label="Пополнение", amount=stars_amount)]
)
logger.info(f"Создан Stars invoice на {stars_amount} звезд (~{amount_rubles:.2f}₽)")
logger.info(f"Создан Stars invoice на {stars_amount} звезд (~{int(amount_rubles)}₽)")
return invoice_link
except Exception as e:
@@ -90,7 +90,7 @@ class PaymentService:
logger.info(f"💰 Баланс пользователя {user.telegram_id} изменен: {old_balance}{user.balance_kopeks} (изменение: +{amount_kopeks})")
description_for_referral = f"Пополнение Stars: {rubles_amount:.2f}₽ ({stars_amount} ⭐)"
description_for_referral = f"Пополнение Stars: {int(rubles_amount)}₽ ({stars_amount} ⭐)"
logger.info(f"🔍 Проверка реферальной логики для описания: '{description_for_referral}'")
if any(word in description_for_referral.lower() for word in ["пополнение", "stars", "yookassa", "topup"]) and not any(word in description_for_referral.lower() for word in ["комиссия", "бонус"]):
@@ -125,13 +125,15 @@ class PaymentService:
f"Баланс пополнен автоматически!",
parse_mode="HTML"
)
logger.info(f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {rubles_amount:.2f}")
logger.info(
f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {int(rubles_amount)}"
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении Stars: {e}")
logger.info(
f"✅ Обработан Stars платеж: пользователь {user_id}, "
f"{stars_amount} звезд → {rubles_amount:.2f}"
f"{stars_amount} звезд → {int(rubles_amount)}"
)
return True
else:
@@ -313,7 +315,7 @@ class PaymentService:
f"Баланс пополнен автоматически!",
parse_mode="HTML"
)
logger.info(f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {updated_payment.amount_kopeks/100:.2f}")
logger.info(f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {updated_payment.amount_kopeks//100}")
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении: {e}")
else:
@@ -352,7 +354,7 @@ class PaymentService:
user = await get_user_by_id(db, payment.user_id)
if user:
await add_user_balance(db, user, payment.amount_kopeks, f"Пополнение YooKassa: {payment.amount_kopeks/100:.2f}")
await add_user_balance(db, user, payment.amount_kopeks, f"Пополнение YooKassa: {payment.amount_kopeks//100}")
logger.info(f"Успешно обработан платеж YooKassa {payment.yookassa_payment_id}: "
f"пользователь {payment.user_id} получил {payment.amount_kopeks/100}")

View File

@@ -221,7 +221,7 @@ class TributeService:
text = (
f"✅ **Платеж успешно получен!**\n\n"
f"💰 Сумма: {amount_rubles:.2f}\n"
f"💰 Сумма: {int(amount_rubles)}\n"
f"💳 Способ оплаты: Tribute\n"
f"🎉 Средства зачислены на баланс!\n\n"
f"Спасибо за оплату! 🙏"
@@ -277,7 +277,7 @@ class TributeService:
text = (
f"🔄 **Возврат средств**\n\n"
f"💰 Сумма возврата: {amount_rubles:.2f}\n"
f"💰 Сумма возврата: {int(amount_rubles)}\n"
f"💳 Способ: Tribute\n\n"
f"Средства будут возвращены на вашу карту в течение 3-5 рабочих дней.\n\n"
f"Если у вас есть вопросы, обратитесь в поддержку."

View File

@@ -25,3 +25,4 @@ APScheduler==3.10.4
python-dateutil==2.8.2
pytz==2023.4
cryptography>=41.0.0
qrcode[pil]==7.4.2