mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-21 11:51:06 +00:00
Revert "Preserve saved cart metadata when normalizing devices"
This commit is contained in:
@@ -146,8 +146,6 @@ TRAFFIC_PACKAGES_CONFIG="5:2000:false,10:3500:false,25:7000:false,50:11000:true,
|
||||
|
||||
# Цена за дополнительное устройство (DEFAULT_DEVICE_LIMIT идет бесплатно!)
|
||||
PRICE_PER_DEVICE=10000
|
||||
# Включить выбор количества устройств при покупке и продлении
|
||||
DEVICES_SELECTION_ENABLED=true
|
||||
|
||||
# ===== РЕФЕРАЛЬНАЯ СИСТЕМА =====
|
||||
REFERRAL_PROGRAM_ENABLED=true
|
||||
|
||||
@@ -569,7 +569,6 @@ BASE_PROMO_GROUP_PERIOD_DISCOUNTS=60:10,90:20,180:40,360:70
|
||||
|
||||
TRAFFIC_PACKAGES_CONFIG="5:2000:false,10:3500:false,25:7000:false,50:11000:true,100:15000:true,0:20000:true"
|
||||
PRICE_PER_DEVICE=5000
|
||||
DEVICES_SELECTION_ENABLED=true
|
||||
|
||||
# ===== РЕФЕРАЛЬНАЯ СИСТЕМА =====
|
||||
REFERRAL_PROGRAM_ENABLED=true
|
||||
|
||||
@@ -123,11 +123,10 @@ class Settings(BaseSettings):
|
||||
PRICE_TRAFFIC_500GB: int = 19000
|
||||
PRICE_TRAFFIC_1000GB: int = 19500
|
||||
PRICE_TRAFFIC_UNLIMITED: int = 20000
|
||||
|
||||
|
||||
TRAFFIC_PACKAGES_CONFIG: str = ""
|
||||
|
||||
PRICE_PER_DEVICE: int = 5000
|
||||
DEVICES_SELECTION_ENABLED: bool = True
|
||||
|
||||
BASE_PROMO_GROUP_PERIOD_DISCOUNTS_ENABLED: bool = False
|
||||
BASE_PROMO_GROUP_PERIOD_DISCOUNTS: str = ""
|
||||
@@ -798,12 +797,9 @@ class Settings(BaseSettings):
|
||||
|
||||
def is_traffic_fixed(self) -> bool:
|
||||
return self.TRAFFIC_SELECTION_MODE.lower() == "fixed"
|
||||
|
||||
|
||||
def get_fixed_traffic_limit(self) -> int:
|
||||
return self.FIXED_TRAFFIC_LIMIT_GB
|
||||
|
||||
def is_devices_selection_enabled(self) -> bool:
|
||||
return self.DEVICES_SELECTION_ENABLED
|
||||
|
||||
def is_yookassa_enabled(self) -> bool:
|
||||
return (self.YOOKASSA_ENABLED and
|
||||
|
||||
@@ -35,7 +35,6 @@ from app.database.crud.server_squad import (
|
||||
get_server_ids_by_uuids,
|
||||
)
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
from app.utils.subscription_utils import resolve_hwid_device_limit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -3833,50 +3832,40 @@ async def admin_buy_subscription_execute(
|
||||
from app.external.remnawave_api import UserStatus, TrafficLimitStrategy
|
||||
remnawave_service = RemnaWaveService()
|
||||
|
||||
hwid_limit = resolve_hwid_device_limit(subscription)
|
||||
|
||||
if target_user.remnawave_uuid:
|
||||
async with remnawave_service.get_api_client() as api:
|
||||
update_kwargs = dict(
|
||||
remnawave_user = await api.update_user(
|
||||
uuid=target_user.remnawave_uuid,
|
||||
status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED,
|
||||
expire_at=subscription.end_date,
|
||||
traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0,
|
||||
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
|
||||
hwid_device_limit=subscription.device_limit,
|
||||
description=settings.format_remnawave_user_description(
|
||||
full_name=target_user.full_name,
|
||||
username=target_user.username,
|
||||
telegram_id=target_user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=subscription.connected_squads
|
||||
)
|
||||
|
||||
if hwid_limit is not None:
|
||||
update_kwargs['hwid_device_limit'] = hwid_limit
|
||||
|
||||
remnawave_user = await api.update_user(**update_kwargs)
|
||||
else:
|
||||
username = f"user_{target_user.telegram_id}"
|
||||
async with remnawave_service.get_api_client() as api:
|
||||
create_kwargs = dict(
|
||||
remnawave_user = await api.create_user(
|
||||
username=username,
|
||||
expire_at=subscription.end_date,
|
||||
status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED,
|
||||
traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0,
|
||||
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
|
||||
telegram_id=target_user.telegram_id,
|
||||
hwid_device_limit=subscription.device_limit,
|
||||
description=settings.format_remnawave_user_description(
|
||||
full_name=target_user.full_name,
|
||||
username=target_user.username,
|
||||
telegram_id=target_user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=subscription.connected_squads
|
||||
)
|
||||
|
||||
if hwid_limit is not None:
|
||||
create_kwargs['hwid_device_limit'] = hwid_limit
|
||||
|
||||
remnawave_user = await api.create_user(**create_kwargs)
|
||||
|
||||
if remnawave_user and hasattr(remnawave_user, 'uuid'):
|
||||
target_user.remnawave_uuid = remnawave_user.uuid
|
||||
|
||||
@@ -111,32 +111,20 @@ async def start_simple_subscription_purchase(
|
||||
subscription_params,
|
||||
resolved_squad_uuid,
|
||||
)
|
||||
show_devices = settings.is_devices_selection_enabled()
|
||||
|
||||
message_lines = [
|
||||
"⚡ <b>Простая покупка подписки</b>",
|
||||
"",
|
||||
f"📅 Период: {subscription_params['period_days']} дней",
|
||||
]
|
||||
|
||||
if show_devices:
|
||||
message_lines.append(f"📱 Устройства: {subscription_params['device_limit']}")
|
||||
|
||||
message_lines.extend([
|
||||
f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}",
|
||||
f"🌍 Сервер: {server_label}",
|
||||
"",
|
||||
f"💰 Стоимость: {settings.format_price(price_kopeks)}",
|
||||
f"💳 Ваш баланс: {settings.format_price(user_balance_kopeks)}",
|
||||
"",
|
||||
(
|
||||
message_text = (
|
||||
f"⚡ <b>Простая покупка подписки</b>\n\n"
|
||||
f"📅 Период: {subscription_params['period_days']} дней\n"
|
||||
f"📱 Устройства: {subscription_params['device_limit']}\n"
|
||||
f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}\n"
|
||||
f"🌍 Сервер: {server_label}\n\n"
|
||||
f"💰 Стоимость: {settings.format_price(price_kopeks)}\n"
|
||||
f"💳 Ваш баланс: {settings.format_price(user_balance_kopeks)}\n\n"
|
||||
+ (
|
||||
"Вы можете оплатить подписку с баланса или выбрать другой способ оплаты."
|
||||
if can_pay_from_balance
|
||||
else "Баланс пока недостаточный для мгновенной оплаты. Выберите подходящий способ оплаты:"
|
||||
),
|
||||
])
|
||||
|
||||
message_text = "\n".join(message_lines)
|
||||
)
|
||||
)
|
||||
|
||||
if trial_notice:
|
||||
message_text = f"{trial_notice}\n\n{message_text}"
|
||||
@@ -445,28 +433,16 @@ async def handle_simple_subscription_pay_with_balance(
|
||||
subscription_params,
|
||||
resolved_squad_uuid,
|
||||
)
|
||||
show_devices = settings.is_devices_selection_enabled()
|
||||
|
||||
success_lines = [
|
||||
"✅ <b>Подписка успешно активирована!</b>",
|
||||
"",
|
||||
f"📅 Период: {subscription_params['period_days']} дней",
|
||||
]
|
||||
|
||||
if show_devices:
|
||||
success_lines.append(f"📱 Устройства: {subscription_params['device_limit']}")
|
||||
|
||||
success_lines.extend([
|
||||
f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}",
|
||||
f"🌍 Сервер: {server_label}",
|
||||
"",
|
||||
f"💰 Списано с баланса: {settings.format_price(price_kopeks)}",
|
||||
f"💳 Ваш баланс: {settings.format_price(db_user.balance_kopeks)}",
|
||||
"",
|
||||
"🔗 Для подключения перейдите в раздел 'Подключиться'",
|
||||
])
|
||||
|
||||
success_message = "\n".join(success_lines)
|
||||
success_message = (
|
||||
f"✅ <b>Подписка успешно активирована!</b>\n\n"
|
||||
f"📅 Период: {subscription_params['period_days']} дней\n"
|
||||
f"📱 Устройства: {subscription_params['device_limit']}\n"
|
||||
f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}\n"
|
||||
f"🌍 Сервер: {server_label}\n\n"
|
||||
f"💰 Списано с баланса: {settings.format_price(price_kopeks)}\n"
|
||||
f"💳 Ваш баланс: {settings.format_price(db_user.balance_kopeks)}\n\n"
|
||||
f"🔗 Для подключения перейдите в раздел 'Подключиться'"
|
||||
)
|
||||
|
||||
connect_mode = settings.CONNECT_BUTTON_MODE
|
||||
subscription_link = get_display_subscription_link(subscription)
|
||||
@@ -643,31 +619,19 @@ async def handle_simple_subscription_other_payment_methods(
|
||||
subscription_params,
|
||||
resolved_squad_uuid,
|
||||
)
|
||||
show_devices = settings.is_devices_selection_enabled()
|
||||
|
||||
message_lines = [
|
||||
"💳 <b>Оплата подписки</b>",
|
||||
"",
|
||||
f"📅 Период: {subscription_params['period_days']} дней",
|
||||
]
|
||||
|
||||
if show_devices:
|
||||
message_lines.append(f"📱 Устройства: {subscription_params['device_limit']}")
|
||||
|
||||
message_lines.extend([
|
||||
f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}",
|
||||
f"🌍 Сервер: {server_label}",
|
||||
"",
|
||||
f"💰 Стоимость: {settings.format_price(price_kopeks)}",
|
||||
"",
|
||||
(
|
||||
message_text = (
|
||||
f"💳 <b>Оплата подписки</b>\n\n"
|
||||
f"📅 Период: {subscription_params['period_days']} дней\n"
|
||||
f"📱 Устройства: {subscription_params['device_limit']}\n"
|
||||
f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}\n"
|
||||
f"🌍 Сервер: {server_label}\n\n"
|
||||
f"💰 Стоимость: {settings.format_price(price_kopeks)}\n\n"
|
||||
+ (
|
||||
"Вы можете оплатить подписку с баланса или выбрать другой способ оплаты:"
|
||||
if can_pay_from_balance
|
||||
else "Выберите подходящий способ оплаты:"
|
||||
),
|
||||
])
|
||||
|
||||
message_text = "\n".join(message_lines)
|
||||
)
|
||||
)
|
||||
|
||||
base_keyboard = _get_simple_subscription_payment_keyboard(db_user.language)
|
||||
keyboard_rows = []
|
||||
@@ -890,25 +854,14 @@ async def handle_simple_subscription_payment_method(
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
|
||||
|
||||
# Подготавливаем текст сообщения
|
||||
show_devices = settings.is_devices_selection_enabled()
|
||||
|
||||
message_lines = [
|
||||
"💳 <b>Оплата подписки через YooKassa</b>",
|
||||
"",
|
||||
f"📅 Период: {subscription_params['period_days']} дней",
|
||||
]
|
||||
|
||||
if show_devices:
|
||||
message_lines.append(f"📱 Устройства: {subscription_params['device_limit']}")
|
||||
|
||||
message_lines.extend([
|
||||
f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}",
|
||||
f"💰 Сумма: {settings.format_price(price_kopeks)}",
|
||||
f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...",
|
||||
"",
|
||||
])
|
||||
|
||||
message_text = "\n".join(message_lines)
|
||||
message_text = (
|
||||
f"💳 <b>Оплата подписки через YooKassa</b>\n\n"
|
||||
f"📅 Период: {subscription_params['period_days']} дней\n"
|
||||
f"📱 Устройства: {subscription_params['device_limit']}\n"
|
||||
f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}\n"
|
||||
f"💰 Сумма: {settings.format_price(price_kopeks)}\n"
|
||||
f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...\n\n"
|
||||
)
|
||||
|
||||
# Добавляем инструкции в зависимости от доступных способов оплаты
|
||||
if not confirmation_url:
|
||||
|
||||
@@ -208,20 +208,39 @@ async def handle_subscription_config_back(
|
||||
await state.set_state(SubscriptionStates.selecting_period)
|
||||
|
||||
elif current_state == SubscriptionStates.selecting_devices.state:
|
||||
await _show_previous_configuration_step(callback, state, db_user, texts, db)
|
||||
|
||||
elif current_state == SubscriptionStates.confirming_purchase.state:
|
||||
if settings.is_devices_selection_enabled():
|
||||
if await _should_show_countries_management(db_user):
|
||||
countries = await _get_available_countries(db_user.promo_group_id)
|
||||
data = await state.get_data()
|
||||
selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
|
||||
selected_countries = data.get('countries', [])
|
||||
|
||||
await callback.message.edit_text(
|
||||
texts.SELECT_DEVICES,
|
||||
reply_markup=get_devices_keyboard(selected_devices, db_user.language)
|
||||
texts.SELECT_COUNTRIES,
|
||||
reply_markup=get_countries_keyboard(countries, selected_countries, db_user.language)
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_devices)
|
||||
await state.set_state(SubscriptionStates.selecting_countries)
|
||||
elif settings.is_traffic_selectable():
|
||||
await callback.message.edit_text(
|
||||
texts.SELECT_TRAFFIC,
|
||||
reply_markup=get_traffic_packages_keyboard(db_user.language)
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_traffic)
|
||||
else:
|
||||
await _show_previous_configuration_step(callback, state, db_user, texts, db)
|
||||
await callback.message.edit_text(
|
||||
await _build_subscription_period_prompt(db_user, texts, db),
|
||||
reply_markup=get_subscription_period_keyboard(db_user.language),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_period)
|
||||
|
||||
elif current_state == SubscriptionStates.confirming_purchase.state:
|
||||
data = await state.get_data()
|
||||
selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
|
||||
|
||||
await callback.message.edit_text(
|
||||
texts.SELECT_DEVICES,
|
||||
reply_markup=get_devices_keyboard(selected_devices, db_user.language)
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_devices)
|
||||
|
||||
else:
|
||||
from app.handlers.menu import show_main_menu
|
||||
@@ -248,37 +267,3 @@ async def handle_subscription_cancel(
|
||||
await show_main_menu(callback, db_user, db)
|
||||
|
||||
await callback.answer("❌ Покупка отменена")
|
||||
async def _show_previous_configuration_step(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
texts,
|
||||
db: AsyncSession,
|
||||
):
|
||||
if await _should_show_countries_management(db_user):
|
||||
countries = await _get_available_countries(db_user.promo_group_id)
|
||||
data = await state.get_data()
|
||||
selected_countries = data.get('countries', [])
|
||||
|
||||
await callback.message.edit_text(
|
||||
texts.SELECT_COUNTRIES,
|
||||
reply_markup=get_countries_keyboard(countries, selected_countries, db_user.language)
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_countries)
|
||||
return
|
||||
|
||||
if settings.is_traffic_selectable():
|
||||
await callback.message.edit_text(
|
||||
texts.SELECT_TRAFFIC,
|
||||
reply_markup=get_traffic_packages_keyboard(db_user.language)
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_traffic)
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
await _build_subscription_period_prompt(db_user, texts, db),
|
||||
reply_markup=get_subscription_period_keyboard(db_user.language),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_period)
|
||||
|
||||
|
||||
@@ -79,7 +79,6 @@ from app.utils.promo_offer import (
|
||||
)
|
||||
|
||||
from .common import _get_addon_discount_percent_for_user, _get_period_hint_from_subscription, logger
|
||||
from .summary import present_subscription_summary
|
||||
|
||||
async def handle_add_countries(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -589,11 +588,6 @@ async def countries_continue(
|
||||
await callback.answer("⚠️ Выберите хотя бы одну страну!", show_alert=True)
|
||||
return
|
||||
|
||||
if not settings.is_devices_selection_enabled():
|
||||
if await present_subscription_summary(callback, state, db_user, texts):
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
|
||||
|
||||
await callback.message.edit_text(
|
||||
|
||||
@@ -183,13 +183,6 @@ async def handle_change_devices(
|
||||
texts = get_texts(db_user.language)
|
||||
subscription = db_user.subscription
|
||||
|
||||
if not settings.is_devices_selection_enabled():
|
||||
await callback.answer(
|
||||
texts.t("DEVICES_SELECTION_DISABLED", "⚠️ Изменение количества устройств недоступно"),
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
|
||||
if not subscription or subscription.is_trial:
|
||||
await callback.answer(
|
||||
texts.t("PAID_FEATURE_ONLY", "⚠️ Эта функция доступна только для платных подписок"),
|
||||
@@ -240,13 +233,6 @@ async def confirm_change_devices(
|
||||
texts = get_texts(db_user.language)
|
||||
subscription = db_user.subscription
|
||||
|
||||
if not settings.is_devices_selection_enabled():
|
||||
await callback.answer(
|
||||
texts.t("DEVICES_SELECTION_DISABLED", "⚠️ Изменение количества устройств недоступно"),
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
|
||||
current_devices = subscription.device_limit
|
||||
|
||||
if new_devices_count == current_devices:
|
||||
@@ -393,13 +379,6 @@ async def execute_change_devices(
|
||||
subscription = db_user.subscription
|
||||
current_devices = subscription.device_limit
|
||||
|
||||
if not settings.is_devices_selection_enabled():
|
||||
await callback.answer(
|
||||
texts.t("DEVICES_SELECTION_DISABLED", "⚠️ Изменение количества устройств недоступно"),
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
if price > 0:
|
||||
success = await subtract_user_balance(
|
||||
@@ -884,13 +863,6 @@ async def confirm_add_devices(
|
||||
texts = get_texts(db_user.language)
|
||||
subscription = db_user.subscription
|
||||
|
||||
if not settings.is_devices_selection_enabled():
|
||||
await callback.answer(
|
||||
texts.t("DEVICES_SELECTION_DISABLED", "⚠️ Изменение количества устройств недоступно"),
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
|
||||
resume_callback = None
|
||||
|
||||
new_total_devices = subscription.device_limit + devices_count
|
||||
|
||||
@@ -159,12 +159,6 @@ async def _prepare_subscription_summary(
|
||||
selected_server_prices.append(total_price_for_server)
|
||||
|
||||
devices_selected = summary_data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
|
||||
devices_selection_enabled = settings.is_devices_selection_enabled()
|
||||
|
||||
if not devices_selection_enabled:
|
||||
devices_selected = settings.DEFAULT_DEVICE_LIMIT
|
||||
|
||||
summary_data['devices'] = devices_selected
|
||||
additional_devices = max(0, devices_selected - settings.DEFAULT_DEVICE_LIMIT)
|
||||
devices_price_per_month = additional_devices * settings.PRICE_PER_DEVICE
|
||||
devices_discount_percent = db_user.get_promo_discount(
|
||||
@@ -281,7 +275,7 @@ async def _prepare_subscription_summary(
|
||||
f" -{texts.format_price(total_servers_discount)})"
|
||||
)
|
||||
details_lines.append(servers_line)
|
||||
if devices_selection_enabled and total_devices_price > 0:
|
||||
if total_devices_price > 0:
|
||||
devices_line = (
|
||||
f"- Доп. устройства: {texts.format_price(devices_price_per_month)}/мес × {months_in_period}"
|
||||
f" = {texts.format_price(total_devices_price)}"
|
||||
@@ -306,28 +300,17 @@ async def _prepare_subscription_summary(
|
||||
|
||||
details_text = "\n".join(details_lines)
|
||||
|
||||
summary_lines = [
|
||||
"📋 <b>Сводка заказа</b>",
|
||||
"",
|
||||
f"📅 <b>Период:</b> {period_display}",
|
||||
f"📊 <b>Трафик:</b> {traffic_display}",
|
||||
f"🌍 <b>Страны:</b> {', '.join(selected_countries_names)}",
|
||||
]
|
||||
|
||||
if devices_selection_enabled:
|
||||
summary_lines.append(f"📱 <b>Устройства:</b> {devices_selected}")
|
||||
|
||||
summary_lines.extend([
|
||||
"",
|
||||
"💰 <b>Детализация стоимости:</b>",
|
||||
details_text,
|
||||
"",
|
||||
f"💎 <b>Общая стоимость:</b> {texts.format_price(total_price)}",
|
||||
"",
|
||||
"Подтверждаете покупку?",
|
||||
])
|
||||
|
||||
summary_text = "\n".join(summary_lines)
|
||||
summary_text = (
|
||||
"📋 <b>Сводка заказа</b>\n\n"
|
||||
f"📅 <b>Период:</b> {period_display}\n"
|
||||
f"📊 <b>Трафик:</b> {traffic_display}\n"
|
||||
f"🌍 <b>Страны:</b> {', '.join(selected_countries_names)}\n"
|
||||
f"📱 <b>Устройства:</b> {devices_selected}\n\n"
|
||||
"💰 <b>Детализация стоимости:</b>\n"
|
||||
f"{details_text}\n\n"
|
||||
f"💎 <b>Общая стоимость:</b> {texts.format_price(total_price)}\n\n"
|
||||
"Подтверждаете покупку?"
|
||||
)
|
||||
|
||||
return summary_text, summary_data
|
||||
|
||||
@@ -427,12 +410,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
|
||||
return 0
|
||||
|
||||
async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSession):
|
||||
devices_selection_enabled = settings.is_devices_selection_enabled()
|
||||
|
||||
if devices_selection_enabled:
|
||||
devices_used = await get_current_devices_count(db_user)
|
||||
else:
|
||||
devices_used = 0
|
||||
devices_used = await get_current_devices_count(db_user)
|
||||
countries_info = await _get_countries_info(subscription.connected_squads)
|
||||
countries_text = ", ".join([c['name'] for c in countries_info]) if countries_info else "Нет"
|
||||
|
||||
@@ -461,18 +439,7 @@ async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSess
|
||||
|
||||
subscription_cost = await get_subscription_cost(subscription, db)
|
||||
|
||||
info_template = texts.SUBSCRIPTION_INFO
|
||||
|
||||
if not devices_selection_enabled:
|
||||
info_template = info_template.replace(
|
||||
"\n📱 <b>Устройства:</b> {devices_used} / {devices_limit}",
|
||||
"",
|
||||
).replace(
|
||||
"\n📱 <b>Devices:</b> {devices_used} / {devices_limit}",
|
||||
"",
|
||||
)
|
||||
|
||||
info_text = info_template.format(
|
||||
info_text = texts.SUBSCRIPTION_INFO.format(
|
||||
status=status_text,
|
||||
type=type_text,
|
||||
end_date=subscription.end_date.strftime("%d.%m.%Y %H:%M"),
|
||||
|
||||
@@ -140,7 +140,6 @@ from .traffic import (
|
||||
handle_switch_traffic,
|
||||
select_traffic,
|
||||
)
|
||||
from .summary import present_subscription_summary
|
||||
|
||||
async def show_subscription_info(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -238,32 +237,26 @@ async def show_subscription_info(
|
||||
devices_list = []
|
||||
devices_count = 0
|
||||
|
||||
show_devices = settings.is_devices_selection_enabled()
|
||||
devices_used_str = ""
|
||||
devices_list: List[Dict[str, Any]] = []
|
||||
try:
|
||||
if db_user.remnawave_uuid:
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
service = RemnaWaveService()
|
||||
|
||||
if show_devices:
|
||||
try:
|
||||
if db_user.remnawave_uuid:
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
service = RemnaWaveService()
|
||||
async with service.get_api_client() as api:
|
||||
response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
|
||||
|
||||
async with service.get_api_client() as api:
|
||||
response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}')
|
||||
if response and 'response' in response:
|
||||
devices_info = response['response']
|
||||
devices_count = devices_info.get('total', 0)
|
||||
devices_list = devices_info.get('devices', [])
|
||||
devices_used_str = str(devices_count)
|
||||
logger.info(f"Найдено {devices_count} устройств для пользователя {db_user.telegram_id}")
|
||||
else:
|
||||
logger.warning(f"Не удалось получить информацию об устройствах для {db_user.telegram_id}")
|
||||
|
||||
if response and 'response' in response:
|
||||
devices_info = response['response']
|
||||
devices_count = devices_info.get('total', 0)
|
||||
devices_list = devices_info.get('devices', [])
|
||||
devices_used_str = str(devices_count)
|
||||
logger.info(f"Найдено {devices_count} устройств для пользователя {db_user.telegram_id}")
|
||||
else:
|
||||
logger.warning(f"Не удалось получить информацию об устройствах для {db_user.telegram_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения устройств для отображения: {e}")
|
||||
devices_used = await get_current_devices_count(db_user)
|
||||
devices_used_str = str(devices_used)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения устройств для отображения: {e}")
|
||||
devices_used_str = await get_current_devices_count(db_user)
|
||||
|
||||
servers_names = await get_servers_display_names(subscription.connected_squads)
|
||||
servers_display = (
|
||||
@@ -272,7 +265,7 @@ async def show_subscription_info(
|
||||
else texts.t("SUBSCRIPTION_NO_SERVERS", "Нет серверов")
|
||||
)
|
||||
|
||||
message_template = texts.t(
|
||||
message = texts.t(
|
||||
"SUBSCRIPTION_OVERVIEW_TEMPLATE",
|
||||
"""👤 {full_name}
|
||||
💰 Баланс: {balance}
|
||||
@@ -285,15 +278,7 @@ async def show_subscription_info(
|
||||
📈 Трафик: {traffic}
|
||||
🌍 Серверы: {servers}
|
||||
📱 Устройства: {devices_used} / {device_limit}""",
|
||||
)
|
||||
|
||||
if not show_devices:
|
||||
message_template = message_template.replace(
|
||||
"\n📱 Устройства: {devices_used} / {device_limit}",
|
||||
"",
|
||||
)
|
||||
|
||||
message = message_template.format(
|
||||
).format(
|
||||
full_name=db_user.full_name,
|
||||
balance=settings.format_price(db_user.balance_kopeks),
|
||||
status_emoji=status_emoji,
|
||||
@@ -308,7 +293,7 @@ async def show_subscription_info(
|
||||
device_limit=subscription.device_limit,
|
||||
)
|
||||
|
||||
if show_devices and devices_list:
|
||||
if devices_list and len(devices_list) > 0:
|
||||
message += "\n\n" + texts.t(
|
||||
"SUBSCRIPTION_CONNECTED_DEVICES_TITLE",
|
||||
"<blockquote>📱 <b>Подключенные устройства:</b>\n",
|
||||
@@ -713,61 +698,7 @@ async def return_to_saved_cart(
|
||||
return
|
||||
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
preserved_metadata_keys = {
|
||||
'saved_cart',
|
||||
'missing_amount',
|
||||
'return_to_cart',
|
||||
'user_id',
|
||||
}
|
||||
preserved_metadata = {
|
||||
key: cart_data[key]
|
||||
for key in preserved_metadata_keys
|
||||
if key in cart_data
|
||||
}
|
||||
|
||||
prepared_cart_data = dict(cart_data)
|
||||
|
||||
if not settings.is_devices_selection_enabled():
|
||||
try:
|
||||
from .pricing import _prepare_subscription_summary
|
||||
|
||||
_, recalculated_data = await _prepare_subscription_summary(
|
||||
db_user,
|
||||
prepared_cart_data,
|
||||
texts,
|
||||
)
|
||||
except ValueError as recalculation_error:
|
||||
logger.error(
|
||||
"Не удалось пересчитать сохраненную корзину пользователя %s: %s",
|
||||
db_user.telegram_id,
|
||||
recalculation_error,
|
||||
)
|
||||
default_limit = max(getattr(settings, "DEFAULT_DEVICE_LIMIT", 1), 1)
|
||||
prepared_cart_data['devices'] = default_limit
|
||||
removed_devices_total = prepared_cart_data.pop('total_devices_price', 0) or 0
|
||||
if removed_devices_total:
|
||||
prepared_cart_data['total_price'] = max(
|
||||
0,
|
||||
prepared_cart_data.get('total_price', 0) - removed_devices_total,
|
||||
)
|
||||
prepared_cart_data.pop('devices_discount_percent', None)
|
||||
prepared_cart_data.pop('devices_discount_total', None)
|
||||
prepared_cart_data.pop('devices_discounted_price_per_month', None)
|
||||
prepared_cart_data.pop('devices_price_per_month', None)
|
||||
else:
|
||||
normalized_cart_data = dict(prepared_cart_data)
|
||||
normalized_cart_data.update(recalculated_data)
|
||||
|
||||
for key, value in preserved_metadata.items():
|
||||
normalized_cart_data[key] = value
|
||||
|
||||
prepared_cart_data = normalized_cart_data
|
||||
|
||||
if prepared_cart_data != cart_data:
|
||||
await user_cart_service.save_user_cart(db_user.id, prepared_cart_data)
|
||||
|
||||
total_price = prepared_cart_data.get('total_price', 0)
|
||||
total_price = cart_data.get('total_price', 0)
|
||||
|
||||
if db_user.balance_kopeks < total_price:
|
||||
missing_amount = total_price - db_user.balance_kopeks
|
||||
@@ -786,45 +717,30 @@ async def return_to_saved_cart(
|
||||
countries = await _get_available_countries(db_user.promo_group_id)
|
||||
selected_countries_names = []
|
||||
|
||||
period_display = format_period_description(prepared_cart_data['period_days'], db_user.language)
|
||||
months_in_period = calculate_months_from_days(cart_data['period_days'])
|
||||
period_display = format_period_description(cart_data['period_days'], db_user.language)
|
||||
|
||||
for country in countries:
|
||||
if country['uuid'] in prepared_cart_data['countries']:
|
||||
if country['uuid'] in cart_data['countries']:
|
||||
selected_countries_names.append(country['name'])
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
traffic_value = prepared_cart_data.get('traffic_gb')
|
||||
if traffic_value is None:
|
||||
traffic_value = settings.get_fixed_traffic_limit()
|
||||
traffic_display = "Безлимитный" if traffic_value == 0 else f"{traffic_value} ГБ"
|
||||
traffic_display = "Безлимитный" if cart_data['traffic_gb'] == 0 else f"{cart_data['traffic_gb']} ГБ"
|
||||
else:
|
||||
traffic_value = prepared_cart_data.get('traffic_gb', 0) or 0
|
||||
traffic_display = "Безлимитный" if traffic_value == 0 else f"{traffic_value} ГБ"
|
||||
traffic_display = "Безлимитный" if cart_data['traffic_gb'] == 0 else f"{cart_data['traffic_gb']} ГБ"
|
||||
|
||||
summary_lines = [
|
||||
"🛒 Восстановленная корзина",
|
||||
"",
|
||||
f"📅 Период: {period_display}",
|
||||
f"📊 Трафик: {traffic_display}",
|
||||
f"🌍 Страны: {', '.join(selected_countries_names)}",
|
||||
]
|
||||
|
||||
if settings.is_devices_selection_enabled():
|
||||
devices_value = prepared_cart_data.get('devices')
|
||||
if devices_value is not None:
|
||||
summary_lines.append(f"📱 Устройства: {devices_value}")
|
||||
|
||||
summary_lines.extend([
|
||||
"",
|
||||
f"💎 Общая стоимость: {texts.format_price(total_price)}",
|
||||
"",
|
||||
"Подтверждаете покупку?",
|
||||
])
|
||||
|
||||
summary_text = "\n".join(summary_lines)
|
||||
summary_text = (
|
||||
"🛒 Восстановленная корзина\n\n"
|
||||
f"📅 Период: {period_display}\n"
|
||||
f"📊 Трафик: {traffic_display}\n"
|
||||
f"🌍 Страны: {', '.join(selected_countries_names)}\n"
|
||||
f"📱 Устройства: {cart_data['devices']}\n\n"
|
||||
f"💎 Общая стоимость: {texts.format_price(total_price)}\n\n"
|
||||
"Подтверждаете покупку?"
|
||||
)
|
||||
|
||||
# Устанавливаем данные в FSM для продолжения процесса
|
||||
await state.set_data(prepared_cart_data)
|
||||
await state.set_data(cart_data)
|
||||
await state.set_state(SubscriptionStates.confirming_purchase)
|
||||
|
||||
await callback.message.edit_text(
|
||||
@@ -956,27 +872,16 @@ async def handle_extend_subscription(
|
||||
texts=texts,
|
||||
)
|
||||
|
||||
renewal_lines = [
|
||||
"⏰ Продление подписки",
|
||||
"",
|
||||
f"Осталось дней: {subscription.days_left}",
|
||||
"",
|
||||
"<b>Ваша текущая конфигурация:</b>",
|
||||
f"🌍 Серверов: {len(subscription.connected_squads)}",
|
||||
f"📊 Трафик: {texts.format_traffic(subscription.traffic_limit_gb)}",
|
||||
]
|
||||
|
||||
if settings.is_devices_selection_enabled():
|
||||
renewal_lines.append(f"📱 Устройств: {subscription.device_limit}")
|
||||
|
||||
renewal_lines.extend([
|
||||
"",
|
||||
"<b>Выберите период продления:</b>",
|
||||
prices_text.rstrip(),
|
||||
"",
|
||||
])
|
||||
|
||||
message_text = "\n".join(renewal_lines)
|
||||
message_text = (
|
||||
"⏰ Продление подписки\n\n"
|
||||
f"Осталось дней: {subscription.days_left}\n\n"
|
||||
f"<b>Ваша текущая конфигурация:</b>\n"
|
||||
f"🌍 Серверов: {len(subscription.connected_squads)}\n"
|
||||
f"📊 Трафик: {texts.format_traffic(subscription.traffic_limit_gb)}\n"
|
||||
f"📱 Устройств: {subscription.device_limit}\n\n"
|
||||
f"<b>Выберите период продления:</b>\n"
|
||||
f"{prices_text.rstrip()}\n\n"
|
||||
)
|
||||
|
||||
if promo_discounts_text:
|
||||
message_text += f"{promo_discounts_text}\n\n"
|
||||
@@ -1343,60 +1248,43 @@ async def select_period(
|
||||
reply_markup=get_traffic_packages_keyboard(db_user.language)
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_traffic)
|
||||
await callback.answer()
|
||||
return
|
||||
else:
|
||||
if await _should_show_countries_management(db_user):
|
||||
countries = await _get_available_countries(db_user.promo_group_id)
|
||||
await callback.message.edit_text(
|
||||
texts.SELECT_COUNTRIES,
|
||||
reply_markup=get_countries_keyboard(countries, [], db_user.language)
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_countries)
|
||||
else:
|
||||
countries = await _get_available_countries(db_user.promo_group_id)
|
||||
available_countries = [c for c in countries if c.get('is_available', True)]
|
||||
data['countries'] = [available_countries[0]['uuid']] if available_countries else []
|
||||
await state.set_data(data)
|
||||
|
||||
if await _should_show_countries_management(db_user):
|
||||
countries = await _get_available_countries(db_user.promo_group_id)
|
||||
await callback.message.edit_text(
|
||||
texts.SELECT_COUNTRIES,
|
||||
reply_markup=get_countries_keyboard(countries, [], db_user.language)
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_countries)
|
||||
await callback.answer()
|
||||
return
|
||||
selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
|
||||
|
||||
countries = await _get_available_countries(db_user.promo_group_id)
|
||||
available_countries = [c for c in countries if c.get('is_available', True)]
|
||||
data['countries'] = [available_countries[0]['uuid']] if available_countries else []
|
||||
await state.set_data(data)
|
||||
await callback.message.edit_text(
|
||||
texts.SELECT_DEVICES,
|
||||
reply_markup=get_devices_keyboard(selected_devices, db_user.language)
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_devices)
|
||||
|
||||
if settings.is_devices_selection_enabled():
|
||||
selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
|
||||
|
||||
await callback.message.edit_text(
|
||||
texts.SELECT_DEVICES,
|
||||
reply_markup=get_devices_keyboard(selected_devices, db_user.language)
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_devices)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
if await present_subscription_summary(callback, state, db_user, texts):
|
||||
await callback.answer()
|
||||
await callback.answer()
|
||||
|
||||
async def select_devices(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.is_devices_selection_enabled():
|
||||
await callback.answer(
|
||||
texts.t("DEVICES_SELECTION_DISABLED", "⚠️ Выбор количества устройств недоступен"),
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
|
||||
if not callback.data.startswith("devices_") or callback.data == "devices_continue":
|
||||
await callback.answer(texts.t("DEVICES_INVALID_REQUEST", "❌ Некорректный запрос"), show_alert=True)
|
||||
await callback.answer("❌ Некорректный запрос", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
devices = int(callback.data.split('_')[1])
|
||||
except (ValueError, IndexError):
|
||||
await callback.answer(texts.t("DEVICES_INVALID_COUNT", "❌ Некорректное количество устройств"), show_alert=True)
|
||||
await callback.answer("❌ Некорректное количество устройств", show_alert=True)
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
@@ -1433,8 +1321,27 @@ async def devices_continue(
|
||||
await callback.answer("⚠️ Некорректный запрос", show_alert=True)
|
||||
return
|
||||
|
||||
if await present_subscription_summary(callback, state, db_user):
|
||||
await callback.answer()
|
||||
data = await state.get_data()
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
try:
|
||||
summary_text, prepared_data = await _prepare_subscription_summary(db_user, data, texts)
|
||||
except ValueError:
|
||||
logger.error(f"Ошибка в расчете цены подписки для пользователя {db_user.telegram_id}")
|
||||
await callback.answer("Ошибка расчета цены. Обратитесь в поддержку.", show_alert=True)
|
||||
return
|
||||
|
||||
await state.set_data(prepared_data)
|
||||
await save_subscription_checkout_draft(db_user.id, prepared_data)
|
||||
|
||||
await callback.message.edit_text(
|
||||
summary_text,
|
||||
reply_markup=get_subscription_confirm_keyboard(db_user.language),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
await state.set_state(SubscriptionStates.confirming_purchase)
|
||||
await callback.answer()
|
||||
|
||||
async def confirm_purchase(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -1529,43 +1436,30 @@ async def confirm_purchase(
|
||||
total_servers_discount = data.get('servers_discount_total', 0)
|
||||
servers_discount_percent = data.get('servers_discount_percent', 0)
|
||||
|
||||
devices_selection_enabled = settings.is_devices_selection_enabled()
|
||||
devices_selected = data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
|
||||
|
||||
if not devices_selection_enabled:
|
||||
devices_selected = settings.DEFAULT_DEVICE_LIMIT
|
||||
|
||||
additional_devices = max(0, devices_selected - settings.DEFAULT_DEVICE_LIMIT)
|
||||
additional_devices = max(0, data['devices'] - settings.DEFAULT_DEVICE_LIMIT)
|
||||
devices_price_per_month = data.get(
|
||||
'devices_price_per_month', additional_devices * settings.PRICE_PER_DEVICE
|
||||
)
|
||||
|
||||
devices_discount_percent = 0
|
||||
discounted_devices_price_per_month = 0
|
||||
devices_discount_total = 0
|
||||
total_devices_price = 0
|
||||
|
||||
if devices_selection_enabled and additional_devices > 0:
|
||||
if 'devices_discount_percent' in data:
|
||||
devices_discount_percent = data.get('devices_discount_percent', 0)
|
||||
discounted_devices_price_per_month = data.get(
|
||||
'devices_discounted_price_per_month', devices_price_per_month
|
||||
)
|
||||
devices_discount_total = data.get('devices_discount_total', 0)
|
||||
total_devices_price = data.get(
|
||||
'total_devices_price', discounted_devices_price_per_month * months_in_period
|
||||
)
|
||||
else:
|
||||
devices_discount_percent = db_user.get_promo_discount(
|
||||
"devices",
|
||||
data['period_days'],
|
||||
)
|
||||
discounted_devices_price_per_month, discount_per_month = apply_percentage_discount(
|
||||
devices_price_per_month,
|
||||
devices_discount_percent,
|
||||
)
|
||||
devices_discount_total = discount_per_month * months_in_period
|
||||
total_devices_price = discounted_devices_price_per_month * months_in_period
|
||||
if 'devices_discount_percent' in data:
|
||||
devices_discount_percent = data.get('devices_discount_percent', 0)
|
||||
discounted_devices_price_per_month = data.get(
|
||||
'devices_discounted_price_per_month', devices_price_per_month
|
||||
)
|
||||
devices_discount_total = data.get('devices_discount_total', 0)
|
||||
total_devices_price = data.get(
|
||||
'total_devices_price', discounted_devices_price_per_month * months_in_period
|
||||
)
|
||||
else:
|
||||
devices_discount_percent = db_user.get_promo_discount(
|
||||
"devices",
|
||||
data['period_days'],
|
||||
)
|
||||
discounted_devices_price_per_month, discount_per_month = apply_percentage_discount(
|
||||
devices_price_per_month,
|
||||
devices_discount_percent,
|
||||
)
|
||||
devices_discount_total = discount_per_month * months_in_period
|
||||
total_devices_price = discounted_devices_price_per_month * months_in_period
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
final_traffic_gb = settings.get_fixed_traffic_limit()
|
||||
@@ -1772,15 +1666,6 @@ async def confirm_purchase(
|
||||
return
|
||||
|
||||
existing_subscription = db_user.subscription
|
||||
if settings.is_devices_selection_enabled():
|
||||
selected_devices = devices_selected
|
||||
else:
|
||||
selected_devices = settings.DEFAULT_DEVICE_LIMIT
|
||||
|
||||
should_update_devices = (
|
||||
settings.is_devices_selection_enabled() and selected_devices is not None
|
||||
)
|
||||
|
||||
was_trial_conversion = False
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
@@ -1823,8 +1708,7 @@ async def confirm_purchase(
|
||||
existing_subscription.is_trial = False
|
||||
existing_subscription.status = SubscriptionStatus.ACTIVE.value
|
||||
existing_subscription.traffic_limit_gb = final_traffic_gb
|
||||
if should_update_devices:
|
||||
existing_subscription.device_limit = selected_devices
|
||||
existing_subscription.device_limit = data['devices']
|
||||
existing_subscription.connected_squads = data['countries']
|
||||
|
||||
existing_subscription.start_date = current_time
|
||||
@@ -1839,24 +1723,11 @@ async def confirm_purchase(
|
||||
|
||||
else:
|
||||
logger.info(f"Создаем новую подписку для пользователя {db_user.telegram_id}")
|
||||
default_device_limit = getattr(settings, "DEFAULT_DEVICE_LIMIT", 1)
|
||||
if should_update_devices:
|
||||
resolved_device_limit = selected_devices
|
||||
else:
|
||||
resolved_device_limit = (
|
||||
selected_devices
|
||||
if selected_devices is not None
|
||||
else default_device_limit
|
||||
)
|
||||
|
||||
if resolved_device_limit is None:
|
||||
resolved_device_limit = default_device_limit
|
||||
|
||||
subscription = await create_paid_subscription_with_traffic_mode(
|
||||
db=db,
|
||||
user_id=db_user.id,
|
||||
duration_days=data['period_days'],
|
||||
device_limit=resolved_device_limit,
|
||||
device_limit=data['devices'],
|
||||
connected_squads=data['countries'],
|
||||
traffic_gb=final_traffic_gb
|
||||
)
|
||||
@@ -1874,11 +1745,11 @@ async def confirm_purchase(
|
||||
await add_user_to_servers(db, server_ids)
|
||||
|
||||
logger.info(f"Сохранены цены серверов за весь период: {server_prices}")
|
||||
|
||||
|
||||
await db.refresh(db_user)
|
||||
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
|
||||
|
||||
if db_user.remnawave_uuid:
|
||||
remnawave_user = await subscription_service.update_remnawave_user(
|
||||
db,
|
||||
@@ -1893,7 +1764,7 @@ async def confirm_purchase(
|
||||
reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT,
|
||||
reset_reason="покупка подписки",
|
||||
)
|
||||
|
||||
|
||||
if not remnawave_user:
|
||||
logger.error(f"Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}")
|
||||
remnawave_user = await subscription_service.create_remnawave_user(
|
||||
@@ -1902,7 +1773,7 @@ async def confirm_purchase(
|
||||
reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT,
|
||||
reset_reason="покупка подписки (повторная попытка)",
|
||||
)
|
||||
|
||||
|
||||
transaction = await create_transaction(
|
||||
db=db,
|
||||
user_id=db_user.id,
|
||||
@@ -1910,7 +1781,7 @@ async def confirm_purchase(
|
||||
amount_kopeks=final_price,
|
||||
description=f"Подписка на {data['period_days']} дней ({months_in_period} мес)"
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_subscription_purchase_notification(
|
||||
@@ -2117,7 +1988,7 @@ async def create_paid_subscription_with_traffic_mode(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
duration_days: int,
|
||||
device_limit: Optional[int],
|
||||
device_limit: int,
|
||||
connected_squads: List[str],
|
||||
traffic_gb: Optional[int] = None
|
||||
):
|
||||
@@ -2131,20 +2002,16 @@ async def create_paid_subscription_with_traffic_mode(
|
||||
else:
|
||||
traffic_limit_gb = traffic_gb
|
||||
|
||||
create_kwargs = dict(
|
||||
subscription = await create_paid_subscription(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
duration_days=duration_days,
|
||||
traffic_limit_gb=traffic_limit_gb,
|
||||
device_limit=device_limit,
|
||||
connected_squads=connected_squads,
|
||||
update_server_counters=False,
|
||||
)
|
||||
|
||||
if device_limit is not None:
|
||||
create_kwargs['device_limit'] = device_limit
|
||||
|
||||
subscription = await create_paid_subscription(**create_kwargs)
|
||||
|
||||
logger.info(f"📋 Создана подписка с трафиком: {traffic_limit_gb} ГБ (режим: {settings.TRAFFIC_SELECTION_MODE})")
|
||||
|
||||
return subscription
|
||||
@@ -2167,14 +2034,9 @@ async def handle_subscription_settings(
|
||||
)
|
||||
return
|
||||
|
||||
show_devices = settings.is_devices_selection_enabled()
|
||||
devices_used = await get_current_devices_count(db_user)
|
||||
|
||||
if show_devices:
|
||||
devices_used = await get_current_devices_count(db_user)
|
||||
else:
|
||||
devices_used = 0
|
||||
|
||||
settings_template = texts.t(
|
||||
settings_text = texts.t(
|
||||
"SUBSCRIPTION_SETTINGS_OVERVIEW",
|
||||
(
|
||||
"⚙️ <b>Настройки подписки</b>\n\n"
|
||||
@@ -2184,15 +2046,7 @@ async def handle_subscription_settings(
|
||||
"📱 Устройства: {devices_used} / {devices_limit}\n\n"
|
||||
"Выберите что хотите изменить:"
|
||||
),
|
||||
)
|
||||
|
||||
if not show_devices:
|
||||
settings_template = settings_template.replace(
|
||||
"\n📱 Устройства: {devices_used} / {devices_limit}",
|
||||
"",
|
||||
)
|
||||
|
||||
settings_text = settings_template.format(
|
||||
).format(
|
||||
countries_count=len(subscription.connected_squads),
|
||||
traffic_used=texts.format_traffic(subscription.traffic_used_gb),
|
||||
traffic_limit=texts.format_traffic(subscription.traffic_limit_gb),
|
||||
@@ -2587,26 +2441,16 @@ async def handle_simple_subscription_purchase(
|
||||
|
||||
if user_balance_kopeks >= price_kopeks:
|
||||
# Если баланс достаточный, предлагаем оплатить с баланса
|
||||
simple_lines = [
|
||||
"⚡ <b>Простая покупка подписки</b>",
|
||||
"",
|
||||
f"📅 Период: {subscription_params['period_days']} дней",
|
||||
]
|
||||
|
||||
if settings.is_devices_selection_enabled():
|
||||
simple_lines.append(f"📱 Устройства: {subscription_params['device_limit']}")
|
||||
|
||||
simple_lines.extend([
|
||||
f"📊 Трафик: {traffic_text}",
|
||||
f"🌍 Сервер: {'Любой доступный' if not subscription_params['squad_uuid'] else 'Выбранный'}",
|
||||
"",
|
||||
f"💰 Стоимость: {settings.format_price(price_kopeks)}",
|
||||
f"💳 Ваш баланс: {settings.format_price(user_balance_kopeks)}",
|
||||
"",
|
||||
"Вы можете оплатить подписку с баланса или выбрать другой способ оплаты.",
|
||||
])
|
||||
|
||||
message_text = "\n".join(simple_lines)
|
||||
message_text = (
|
||||
f"⚡ <b>Простая покупка подписки</b>\n\n"
|
||||
f"📅 Период: {subscription_params['period_days']} дней\n"
|
||||
f"📱 Устройства: {subscription_params['device_limit']}\n"
|
||||
f"📊 Трафик: {traffic_text}\n"
|
||||
f"🌍 Сервер: {'Любой доступный' if not subscription_params['squad_uuid'] else 'Выбранный'}\n\n"
|
||||
f"💰 Стоимость: {settings.format_price(price_kopeks)}\n"
|
||||
f"💳 Ваш баланс: {settings.format_price(user_balance_kopeks)}\n\n"
|
||||
f"Вы можете оплатить подписку с баланса или выбрать другой способ оплаты."
|
||||
)
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="✅ Оплатить с баланса", callback_data="simple_subscription_pay_with_balance")],
|
||||
@@ -2615,26 +2459,16 @@ async def handle_simple_subscription_purchase(
|
||||
])
|
||||
else:
|
||||
# Если баланс недостаточный, предлагаем внешние способы оплаты
|
||||
simple_lines = [
|
||||
"⚡ <b>Простая покупка подписки</b>",
|
||||
"",
|
||||
f"📅 Период: {subscription_params['period_days']} дней",
|
||||
]
|
||||
|
||||
if settings.is_devices_selection_enabled():
|
||||
simple_lines.append(f"📱 Устройства: {subscription_params['device_limit']}")
|
||||
|
||||
simple_lines.extend([
|
||||
f"📊 Трафик: {traffic_text}",
|
||||
f"🌍 Сервер: {'Любой доступный' if not subscription_params['squad_uuid'] else 'Выбранный'}",
|
||||
"",
|
||||
f"💰 Стоимость: {settings.format_price(price_kopeks)}",
|
||||
f"💳 Ваш баланс: {settings.format_price(user_balance_kopeks)}",
|
||||
"",
|
||||
"Выберите способ оплаты:",
|
||||
])
|
||||
|
||||
message_text = "\n".join(simple_lines)
|
||||
message_text = (
|
||||
f"⚡ <b>Простая покупка подписки</b>\n\n"
|
||||
f"📅 Период: {subscription_params['period_days']} дней\n"
|
||||
f"📱 Устройства: {subscription_params['device_limit']}\n"
|
||||
f"📊 Трафик: {traffic_text}\n"
|
||||
f"🌍 Сервер: {'Любой доступный' if not subscription_params['squad_uuid'] else 'Выбранный'}\n\n"
|
||||
f"💰 Стоимость: {settings.format_price(price_kopeks)}\n"
|
||||
f"💳 Ваш баланс: {settings.format_price(user_balance_kopeks)}\n\n"
|
||||
f"Выберите способ оплаты:"
|
||||
)
|
||||
|
||||
keyboard = _get_simple_subscription_payment_keyboard(db_user.language)
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import logging
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from aiogram import types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from app.localization.texts import get_texts
|
||||
from app.services.subscription_checkout_service import save_subscription_checkout_draft
|
||||
from app.states import SubscriptionStates
|
||||
from app.keyboards.inline import get_subscription_confirm_keyboard
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover - only for type checking
|
||||
from .pricing import _prepare_subscription_summary
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def present_subscription_summary(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user,
|
||||
texts: Optional = None,
|
||||
) -> bool:
|
||||
"""Render the subscription purchase summary and switch to the confirmation state.
|
||||
|
||||
Returns ``True`` when the summary is shown successfully and ``False`` if
|
||||
calculation failed (an error is shown to the user in this case).
|
||||
"""
|
||||
|
||||
if texts is None:
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
data = await state.get_data()
|
||||
|
||||
from .pricing import _prepare_subscription_summary
|
||||
|
||||
try:
|
||||
summary_text, prepared_data = await _prepare_subscription_summary(db_user, data, texts)
|
||||
except ValueError as exc:
|
||||
logger.error(
|
||||
"Ошибка в расчете цены подписки для пользователя %s: %s",
|
||||
db_user.telegram_id,
|
||||
exc,
|
||||
)
|
||||
await callback.answer("Ошибка расчета цены. Обратитесь в поддержку.", show_alert=True)
|
||||
return False
|
||||
|
||||
await state.set_data(prepared_data)
|
||||
await save_subscription_checkout_draft(db_user.id, prepared_data)
|
||||
|
||||
await callback.message.edit_text(
|
||||
summary_text,
|
||||
reply_markup=get_subscription_confirm_keyboard(db_user.language),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
await state.set_state(SubscriptionStates.confirming_purchase)
|
||||
return True
|
||||
@@ -80,7 +80,6 @@ from app.utils.promo_offer import (
|
||||
|
||||
from .common import _apply_addon_discount, _get_addon_discount_percent_for_user, _get_period_hint_from_subscription, get_confirm_switch_traffic_keyboard, get_traffic_switch_keyboard, logger
|
||||
from .countries import _get_available_countries, _should_show_countries_management
|
||||
from .summary import present_subscription_summary
|
||||
|
||||
async def handle_add_traffic(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -353,15 +352,12 @@ async def select_traffic(
|
||||
reply_markup=get_countries_keyboard(countries, [], db_user.language)
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_countries)
|
||||
await callback.answer()
|
||||
return
|
||||
else:
|
||||
countries = await _get_available_countries(db_user.promo_group_id)
|
||||
available_countries = [c for c in countries if c.get('is_available', True)]
|
||||
data['countries'] = [available_countries[0]['uuid']] if available_countries else []
|
||||
await state.set_data(data)
|
||||
|
||||
countries = await _get_available_countries(db_user.promo_group_id)
|
||||
available_countries = [c for c in countries if c.get('is_available', True)]
|
||||
data['countries'] = [available_countries[0]['uuid']] if available_countries else []
|
||||
await state.set_data(data)
|
||||
|
||||
if settings.is_devices_selection_enabled():
|
||||
selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
|
||||
|
||||
await callback.message.edit_text(
|
||||
@@ -369,11 +365,8 @@ async def select_traffic(
|
||||
reply_markup=get_devices_keyboard(selected_devices, db_user.language)
|
||||
)
|
||||
await state.set_state(SubscriptionStates.selecting_devices)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
if await present_subscription_summary(callback, state, db_user, texts):
|
||||
await callback.answer()
|
||||
await callback.answer()
|
||||
|
||||
async def add_traffic(
|
||||
callback: types.CallbackQuery,
|
||||
|
||||
@@ -2085,38 +2085,33 @@ def get_updated_subscription_settings_keyboard(language: str = DEFAULT_LANGUAGE,
|
||||
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
|
||||
|
||||
if show_countries_management:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.t("ADD_COUNTRIES_BUTTON", "🌐 Добавить страны"), callback_data="subscription_add_countries")
|
||||
])
|
||||
|
||||
keyboard.extend([
|
||||
[
|
||||
InlineKeyboardButton(text=texts.t("CHANGE_DEVICES_BUTTON", "📱 Изменить устройства"), callback_data="subscription_change_devices")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text=texts.t("MANAGE_DEVICES_BUTTON", "🔧 Управление устройствами"), callback_data="subscription_manage_devices")
|
||||
]
|
||||
])
|
||||
|
||||
if settings.is_traffic_selectable():
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.t("RESET_TRAFFIC_BUTTON", "🔄 Сбросить трафик"), callback_data="subscription_reset_traffic")
|
||||
])
|
||||
keyboard.append([
|
||||
keyboard.insert(-2, [
|
||||
InlineKeyboardButton(text=texts.t("SWITCH_TRAFFIC_BUTTON", "🔄 Переключить трафик"), callback_data="subscription_switch_traffic")
|
||||
])
|
||||
|
||||
if settings.is_devices_selection_enabled():
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text=texts.t("CHANGE_DEVICES_BUTTON", "📱 Изменить устройства"),
|
||||
callback_data="subscription_change_devices"
|
||||
)
|
||||
keyboard.insert(-2, [
|
||||
InlineKeyboardButton(text=texts.t("RESET_TRAFFIC_BUTTON", "🔄 Сбросить трафик"), callback_data="subscription_reset_traffic")
|
||||
])
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text=texts.t("MANAGE_DEVICES_BUTTON", "🔧 Управление устройствами"),
|
||||
callback_data="subscription_manage_devices"
|
||||
)
|
||||
])
|
||||
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription")
|
||||
])
|
||||
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
|
||||
@@ -856,9 +856,6 @@
|
||||
"DEVICE_CHANGE_NO_REFUND": "Payments are not refunded",
|
||||
"DEVICE_CHANGE_NO_REFUND_INFO": "ℹ️ Payments are not refunded",
|
||||
"DEVICE_CHANGE_RESULT_LINE": "📱 Was: {old} → Now: {new}\n",
|
||||
"DEVICES_INVALID_REQUEST": "❌ Invalid request",
|
||||
"DEVICES_INVALID_COUNT": "❌ Invalid device count",
|
||||
"DEVICES_SELECTION_DISABLED": "⚠️ Device selection is unavailable",
|
||||
"DEVICE_CONNECTION_HELP": "❓ How to reconnect a device?",
|
||||
"DEVICE_FETCH_ERROR": "❌ Failed to load devices",
|
||||
"DEVICE_FETCH_INFO_ERROR": "❌ Failed to load device information",
|
||||
|
||||
@@ -856,9 +856,6 @@
|
||||
"DEVICE_CHANGE_NO_REFUND": "Возврат средств не производится",
|
||||
"DEVICE_CHANGE_NO_REFUND_INFO": "ℹ️ Возврат средств не производится",
|
||||
"DEVICE_CHANGE_RESULT_LINE": "📱 Было: {old} → Стало: {new}\n",
|
||||
"DEVICES_INVALID_REQUEST": "❌ Некорректный запрос",
|
||||
"DEVICES_INVALID_COUNT": "❌ Некорректное количество устройств",
|
||||
"DEVICES_SELECTION_DISABLED": "⚠️ Выбор количества устройств недоступен",
|
||||
"DEVICE_CONNECTION_HELP": "❓ Как подключить устройство заново?",
|
||||
"DEVICE_FETCH_ERROR": "❌ Ошибка получения устройств",
|
||||
"DEVICE_FETCH_INFO_ERROR": "❌ Ошибка получения информации об устройствах",
|
||||
|
||||
@@ -38,7 +38,6 @@ from app.database.crud.user import (
|
||||
subtract_user_balance,
|
||||
cleanup_expired_promo_offer_discounts,
|
||||
)
|
||||
from app.utils.subscription_utils import resolve_hwid_device_limit
|
||||
from app.database.models import MonitoringLog, SubscriptionStatus, Subscription, User, Ticket, TicketStatus
|
||||
from app.localization.texts import get_texts
|
||||
from app.services.notification_settings_service import NotificationSettingsService
|
||||
@@ -284,26 +283,20 @@ class MonitoringService:
|
||||
logger.info(f"📝 Статус подписки {subscription.id} обновлен на 'expired'")
|
||||
|
||||
async with self.api as api:
|
||||
hwid_limit = resolve_hwid_device_limit(subscription)
|
||||
|
||||
update_kwargs = dict(
|
||||
updated_user = await api.update_user(
|
||||
uuid=user.remnawave_uuid,
|
||||
status=UserStatus.ACTIVE if is_active else UserStatus.EXPIRED,
|
||||
expire_at=subscription.end_date,
|
||||
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
|
||||
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
|
||||
hwid_device_limit=subscription.device_limit,
|
||||
description=settings.format_remnawave_user_description(
|
||||
full_name=user.full_name,
|
||||
username=user.username,
|
||||
telegram_id=user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=subscription.connected_squads
|
||||
)
|
||||
|
||||
if hwid_limit is not None:
|
||||
update_kwargs['hwid_device_limit'] = hwid_limit
|
||||
|
||||
updated_user = await api.update_user(**update_kwargs)
|
||||
|
||||
subscription.subscription_url = updated_user.subscription_url
|
||||
subscription.subscription_crypto_link = updated_user.happ_crypto_link
|
||||
|
||||
@@ -38,7 +38,6 @@ from app.database.models import (
|
||||
SubscriptionStatus,
|
||||
ServerSquad,
|
||||
)
|
||||
from app.utils.subscription_utils import resolve_hwid_device_limit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1217,53 +1216,44 @@ class RemnaWaveService:
|
||||
for user in users:
|
||||
if not user.subscription:
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
subscription = user.subscription
|
||||
hwid_limit = resolve_hwid_device_limit(subscription)
|
||||
|
||||
|
||||
if user.remnawave_uuid:
|
||||
update_kwargs = dict(
|
||||
await api.update_user(
|
||||
uuid=user.remnawave_uuid,
|
||||
status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED,
|
||||
expire_at=subscription.end_date,
|
||||
traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0,
|
||||
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
|
||||
hwid_device_limit=subscription.device_limit,
|
||||
description=settings.format_remnawave_user_description(
|
||||
full_name=user.full_name,
|
||||
username=user.username,
|
||||
telegram_id=user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=subscription.connected_squads
|
||||
)
|
||||
|
||||
if hwid_limit is not None:
|
||||
update_kwargs['hwid_device_limit'] = hwid_limit
|
||||
|
||||
await api.update_user(**update_kwargs)
|
||||
stats["updated"] += 1
|
||||
else:
|
||||
username = f"user_{user.telegram_id}"
|
||||
|
||||
create_kwargs = dict(
|
||||
|
||||
new_user = await api.create_user(
|
||||
username=username,
|
||||
expire_at=subscription.end_date,
|
||||
status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED,
|
||||
traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0,
|
||||
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
|
||||
telegram_id=user.telegram_id,
|
||||
hwid_device_limit=subscription.device_limit,
|
||||
description=settings.format_remnawave_user_description(
|
||||
full_name=user.full_name,
|
||||
username=user.username,
|
||||
telegram_id=user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=subscription.connected_squads
|
||||
)
|
||||
|
||||
if hwid_limit is not None:
|
||||
create_kwargs['hwid_device_limit'] = hwid_limit
|
||||
|
||||
new_user = await api.create_user(**create_kwargs)
|
||||
|
||||
await update_user(db, user, remnawave_uuid=new_user.uuid)
|
||||
subscription.remnawave_short_uuid = new_user.short_uuid
|
||||
|
||||
@@ -17,7 +17,6 @@ from app.utils.pricing_utils import (
|
||||
calculate_prorated_price,
|
||||
validate_pricing_calculation
|
||||
)
|
||||
from app.utils.subscription_utils import resolve_hwid_device_limit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -173,7 +172,6 @@ class SubscriptionService:
|
||||
return None
|
||||
|
||||
async with self.get_api_client() as api:
|
||||
hwid_limit = resolve_hwid_device_limit(subscription)
|
||||
existing_users = await api.get_user_by_telegram_id(user.telegram_id)
|
||||
if existing_users:
|
||||
logger.info(f"🔄 Найден существующий пользователь в панели для {user.telegram_id}")
|
||||
@@ -185,24 +183,20 @@ class SubscriptionService:
|
||||
except Exception as hwid_error:
|
||||
logger.warning(f"⚠️ Не удалось сбросить HWID: {hwid_error}")
|
||||
|
||||
update_kwargs = dict(
|
||||
updated_user = await api.update_user(
|
||||
uuid=remnawave_user.uuid,
|
||||
status=UserStatus.ACTIVE,
|
||||
expire_at=subscription.end_date,
|
||||
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
|
||||
traffic_limit_strategy=get_traffic_reset_strategy(),
|
||||
hwid_device_limit=subscription.device_limit,
|
||||
description=settings.format_remnawave_user_description(
|
||||
full_name=user.full_name,
|
||||
username=user.username,
|
||||
telegram_id=user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=subscription.connected_squads
|
||||
)
|
||||
|
||||
if hwid_limit is not None:
|
||||
update_kwargs['hwid_device_limit'] = hwid_limit
|
||||
|
||||
updated_user = await api.update_user(**update_kwargs)
|
||||
|
||||
if reset_traffic:
|
||||
await self._reset_user_traffic(
|
||||
@@ -215,26 +209,22 @@ class SubscriptionService:
|
||||
else:
|
||||
logger.info(f"🆕 Создаем нового пользователя в панели для {user.telegram_id}")
|
||||
username = f"user_{user.telegram_id}"
|
||||
create_kwargs = dict(
|
||||
updated_user = await api.create_user(
|
||||
username=username,
|
||||
expire_at=subscription.end_date,
|
||||
status=UserStatus.ACTIVE,
|
||||
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
|
||||
traffic_limit_strategy=get_traffic_reset_strategy(),
|
||||
telegram_id=user.telegram_id,
|
||||
hwid_device_limit=subscription.device_limit,
|
||||
description=settings.format_remnawave_user_description(
|
||||
full_name=user.full_name,
|
||||
username=user.username,
|
||||
telegram_id=user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=subscription.connected_squads
|
||||
)
|
||||
|
||||
if hwid_limit is not None:
|
||||
create_kwargs['hwid_device_limit'] = hwid_limit
|
||||
|
||||
updated_user = await api.create_user(**create_kwargs)
|
||||
|
||||
if reset_traffic:
|
||||
await self._reset_user_traffic(
|
||||
api,
|
||||
@@ -292,26 +282,20 @@ class SubscriptionService:
|
||||
logger.info(f"🔔 Статус подписки {subscription.id} автоматически изменен на 'expired'")
|
||||
|
||||
async with self.get_api_client() as api:
|
||||
hwid_limit = resolve_hwid_device_limit(subscription)
|
||||
|
||||
update_kwargs = dict(
|
||||
updated_user = await api.update_user(
|
||||
uuid=user.remnawave_uuid,
|
||||
status=UserStatus.ACTIVE if is_actually_active else UserStatus.EXPIRED,
|
||||
expire_at=subscription.end_date,
|
||||
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
|
||||
traffic_limit_strategy=get_traffic_reset_strategy(),
|
||||
hwid_device_limit=subscription.device_limit,
|
||||
description=settings.format_remnawave_user_description(
|
||||
full_name=user.full_name,
|
||||
username=user.username,
|
||||
telegram_id=user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=subscription.connected_squads
|
||||
)
|
||||
|
||||
if hwid_limit is not None:
|
||||
update_kwargs['hwid_device_limit'] = hwid_limit
|
||||
|
||||
updated_user = await api.update_user(**update_kwargs)
|
||||
|
||||
if reset_traffic:
|
||||
await self._reset_user_traffic(
|
||||
|
||||
@@ -195,7 +195,6 @@ class BotConfigurationService:
|
||||
"DEFAULT_TRAFFIC_LIMIT_GB": "SUBSCRIPTIONS_CORE",
|
||||
"MAX_DEVICES_LIMIT": "SUBSCRIPTIONS_CORE",
|
||||
"PRICE_PER_DEVICE": "SUBSCRIPTIONS_CORE",
|
||||
"DEVICES_SELECTION_ENABLED": "SUBSCRIPTIONS_CORE",
|
||||
"BASE_SUBSCRIPTION_PRICE": "SUBSCRIPTIONS_CORE",
|
||||
"DEFAULT_TRAFFIC_RESET_STRATEGY": "TRAFFIC",
|
||||
"RESET_TRAFFIC_ON_PAYMENT": "TRAFFIC",
|
||||
@@ -451,12 +450,6 @@ class BotConfigurationService:
|
||||
"example": "d4aa2b8c-9a36-4f31-93a2-6f07dad05fba",
|
||||
"warning": "Убедитесь, что выбранный сквад активен и доступен для подписки.",
|
||||
},
|
||||
"DEVICES_SELECTION_ENABLED": {
|
||||
"description": "Разрешает пользователям выбирать количество устройств при покупке и продлении подписки.",
|
||||
"format": "Булево значение.",
|
||||
"example": "false",
|
||||
"warning": "При отключении пользователи не смогут докупать устройства из интерфейса бота.",
|
||||
},
|
||||
"CRYPTOBOT_ENABLED": {
|
||||
"description": "Разрешает принимать криптоплатежи через CryptoBot.",
|
||||
"format": "Булево значение.",
|
||||
|
||||
@@ -174,19 +174,3 @@ def convert_subscription_link_to_happ_scheme(subscription_link: Optional[str]) -
|
||||
return subscription_link
|
||||
|
||||
return urlunparse(parsed_link._replace(scheme="happ"))
|
||||
|
||||
|
||||
def resolve_hwid_device_limit(subscription: Optional[Subscription]) -> Optional[int]:
|
||||
"""Return a device limit value for RemnaWave payloads when selection is enabled."""
|
||||
|
||||
if subscription is None:
|
||||
return None
|
||||
|
||||
if not settings.is_devices_selection_enabled():
|
||||
return None
|
||||
|
||||
limit = getattr(subscription, "device_limit", None)
|
||||
if limit is None or limit <= 0:
|
||||
return None
|
||||
|
||||
return limit
|
||||
|
||||
@@ -142,80 +142,6 @@ async def test_return_to_saved_cart_success(mock_callback_query, mock_state, moc
|
||||
# В успешном сценарии вызывается callback.answer()
|
||||
mock_callback_query.answer.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_return_to_saved_cart_normalizes_devices_when_disabled(
|
||||
mock_callback_query,
|
||||
mock_state,
|
||||
mock_user,
|
||||
mock_db,
|
||||
):
|
||||
cart_data = {
|
||||
'period_days': 30,
|
||||
'countries': ['ru', 'us'],
|
||||
'devices': 5,
|
||||
'traffic_gb': 20,
|
||||
'total_price': 45000,
|
||||
'total_devices_price': 15000,
|
||||
'saved_cart': True,
|
||||
'user_id': mock_user.id,
|
||||
}
|
||||
|
||||
sanitized_summary_data = {
|
||||
'period_days': 30,
|
||||
'countries': ['ru', 'us'],
|
||||
'devices': 3,
|
||||
'traffic_gb': 20,
|
||||
'total_price': 30000,
|
||||
'total_devices_price': 0,
|
||||
}
|
||||
|
||||
with patch('app.handlers.subscription.purchase.user_cart_service') as mock_cart_service, \
|
||||
patch('app.handlers.subscription.purchase._get_available_countries') as mock_get_countries, \
|
||||
patch('app.handlers.subscription.purchase.format_period_description') as mock_format_period, \
|
||||
patch('app.localization.texts.get_texts') as mock_get_texts, \
|
||||
patch('app.handlers.subscription.purchase.get_subscription_confirm_keyboard_with_cart') as mock_keyboard_func, \
|
||||
patch('app.handlers.subscription.purchase.settings') as mock_settings, \
|
||||
patch('app.handlers.subscription.pricing._prepare_subscription_summary', new=AsyncMock(return_value=("ignored", sanitized_summary_data))):
|
||||
|
||||
mock_cart_service.get_user_cart = AsyncMock(return_value=cart_data)
|
||||
mock_cart_service.save_user_cart = AsyncMock()
|
||||
mock_get_countries.return_value = [{'uuid': 'ru', 'name': 'Russia'}, {'uuid': 'us', 'name': 'USA'}]
|
||||
mock_format_period.return_value = "30 дней"
|
||||
mock_keyboard = AsyncMock()
|
||||
mock_keyboard_func.return_value = mock_keyboard
|
||||
|
||||
mock_texts = AsyncMock()
|
||||
mock_texts.format_price = lambda x: f"{x/100} ₽"
|
||||
mock_texts.t = lambda key, default=None: default or ""
|
||||
mock_get_texts.return_value = mock_texts
|
||||
|
||||
mock_settings.is_devices_selection_enabled.return_value = False
|
||||
mock_settings.DEFAULT_DEVICE_LIMIT = 3
|
||||
mock_settings.is_traffic_fixed.return_value = False
|
||||
mock_settings.get_fixed_traffic_limit.return_value = 0
|
||||
|
||||
mock_user.balance_kopeks = 60000
|
||||
|
||||
await return_to_saved_cart(mock_callback_query, mock_state, mock_user, mock_db)
|
||||
|
||||
mock_cart_service.save_user_cart.assert_called_once()
|
||||
_, saved_payload = mock_cart_service.save_user_cart.call_args[0]
|
||||
assert saved_payload['devices'] == 3
|
||||
assert saved_payload['total_price'] == 30000
|
||||
assert saved_payload['saved_cart'] is True
|
||||
|
||||
mock_state.set_data.assert_called_once()
|
||||
normalized_data = mock_state.set_data.call_args[0][0]
|
||||
assert normalized_data['devices'] == 3
|
||||
assert normalized_data['total_price'] == 30000
|
||||
assert normalized_data['saved_cart'] is True
|
||||
|
||||
edited_text = mock_callback_query.message.edit_text.call_args[0][0]
|
||||
assert "📱" not in edited_text
|
||||
|
||||
mock_callback_query.answer.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_return_to_saved_cart_insufficient_funds(mock_callback_query, mock_state, mock_user, mock_db):
|
||||
"""Тест возврата к сохраненной корзине с недостаточным балансом"""
|
||||
|
||||
Reference in New Issue
Block a user