Revert "Preserve saved cart metadata when normalizing devices"

This commit is contained in:
Egor
2025-10-31 17:42:04 +03:00
committed by GitHub
parent ab00a0d136
commit 4cc6778e45
21 changed files with 275 additions and 795 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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(

View File

@@ -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

View File

@@ -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"),

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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",

View File

@@ -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": "❌ Ошибка получения информации об устройствах",

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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": "Булево значение.",

View File

@@ -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

View File

@@ -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):
"""Тест возврата к сохраненной корзине с недостаточным балансом"""