Merge pull request #1615 from Fr1ngg/revert-1613-el6wtr-bedolaga/add-option-to-disable-device-selection

Revert "Align simple subscription device caps with disabled mode"
This commit is contained in:
Egor
2025-10-31 18:26:39 +03:00
committed by GitHub
28 changed files with 314 additions and 1113 deletions

View File

@@ -146,10 +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
# Единое количество устройств для режима без выбора (0 — не назначать устройства)
DEVICES_SELECTION_DISABLED_AMOUNT=0
# ===== РЕФЕРАЛЬНАЯ СИСТЕМА =====
REFERRAL_PROGRAM_ENABLED=true

View File

@@ -569,9 +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
# Единое количество устройств для режима без выбора (0 — не назначать устройства)
DEVICES_SELECTION_DISABLED_AMOUNT=0
# ===== РЕФЕРАЛЬНАЯ СИСТЕМА =====
REFERRAL_PROGRAM_ENABLED=true

View File

@@ -123,12 +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
DEVICES_SELECTION_DISABLED_AMOUNT: int = 0
BASE_PROMO_GROUP_PERIOD_DISCOUNTS_ENABLED: bool = False
BASE_PROMO_GROUP_PERIOD_DISCOUNTS: str = ""
@@ -799,31 +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 get_devices_selection_disabled_amount(self) -> int:
try:
value = int(self.DEVICES_SELECTION_DISABLED_AMOUNT)
except (TypeError, ValueError):
value = 0
if value < 0:
return 0
return value
def get_disabled_mode_device_limit(self) -> Optional[int]:
amount = self.get_devices_selection_disabled_amount()
if amount <= 0:
return None
return amount
def is_yookassa_enabled(self) -> bool:
return (self.YOOKASSA_ENABLED and

View File

@@ -41,14 +41,13 @@ async def create_trial_subscription(
user_id: int,
duration_days: int = None,
traffic_limit_gb: int = None,
device_limit: Optional[int] = None,
device_limit: int = None,
squad_uuid: str = None
) -> Subscription:
duration_days = duration_days or settings.TRIAL_DURATION_DAYS
traffic_limit_gb = traffic_limit_gb or settings.TRIAL_TRAFFIC_LIMIT_GB
if device_limit is None:
device_limit = settings.TRIAL_DEVICE_LIMIT
device_limit = device_limit or settings.TRIAL_DEVICE_LIMIT
if not squad_uuid:
try:
from app.database.crud.server_squad import get_random_trial_squad_uuid
@@ -127,16 +126,13 @@ async def create_paid_subscription(
user_id: int,
duration_days: int,
traffic_limit_gb: int = 0,
device_limit: Optional[int] = None,
device_limit: int = 1,
connected_squads: List[str] = None,
update_server_counters: bool = False,
) -> Subscription:
end_date = datetime.utcnow() + timedelta(days=duration_days)
if device_limit is None:
device_limit = settings.DEFAULT_DEVICE_LIMIT
subscription = Subscription(
user_id=user_id,
status=SubscriptionStatus.ACTIVE.value,

View File

@@ -46,9 +46,6 @@ def _format_campaign_summary(campaign, texts) -> str:
bonus_info = f"💰 Бонус на баланс: <b>{bonus_text}</b>"
else:
traffic_text = texts.format_traffic(campaign.subscription_traffic_gb or 0)
device_limit = campaign.subscription_device_limit
if device_limit is None:
device_limit = settings.DEFAULT_DEVICE_LIMIT
bonus_info = (
"📱 Подписка: <b>{days} д.</b>\n"
"🌐 Трафик: <b>{traffic}</b>\n"
@@ -56,7 +53,7 @@ def _format_campaign_summary(campaign, texts) -> str:
).format(
days=campaign.subscription_duration_days or 0,
traffic=traffic_text,
devices=device_limit,
devices=campaign.subscription_device_limit or settings.DEFAULT_DEVICE_LIMIT,
)
return (
@@ -938,9 +935,7 @@ async def start_edit_campaign_subscription_devices(
campaign_edit_message_is_caption=is_caption,
)
current_devices = campaign.subscription_device_limit
if current_devices is None:
current_devices = settings.DEFAULT_DEVICE_LIMIT
current_devices = campaign.subscription_device_limit or settings.DEFAULT_DEVICE_LIMIT
await callback.message.edit_text(
(

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__)
@@ -3339,15 +3338,7 @@ async def _grant_trial_subscription(db: AsyncSession, user_id: int, admin_id: in
logger.error(f"У пользователя {user_id} уже есть подписка")
return False
forced_devices = None
if not settings.is_devices_selection_enabled():
forced_devices = settings.get_devices_selection_disabled_amount()
subscription = await create_trial_subscription(
db,
user_id,
device_limit=forced_devices,
)
subscription = await create_trial_subscription(db, user_id)
subscription_service = SubscriptionService()
await subscription_service.create_remnawave_user(db, subscription)
@@ -3391,20 +3382,12 @@ async def _grant_paid_subscription(db: AsyncSession, user_id: int, days: int, ad
if getattr(settings, "TRIAL_SQUAD_UUID", None):
trial_squads = [settings.TRIAL_SQUAD_UUID]
forced_devices = None
if not settings.is_devices_selection_enabled():
forced_devices = settings.get_devices_selection_disabled_amount()
device_limit = settings.DEFAULT_DEVICE_LIMIT
if forced_devices is not None:
device_limit = forced_devices
subscription = await create_paid_subscription(
db=db,
user_id=user_id,
duration_days=days,
traffic_limit_gb=settings.DEFAULT_TRAFFIC_LIMIT_GB,
device_limit=device_limit,
device_limit=settings.DEFAULT_DEVICE_LIMIT,
connected_squads=trial_squads,
update_server_counters=True,
)
@@ -3611,9 +3594,7 @@ async def admin_buy_subscription(
text += f"👤 {target_user.full_name} (ID: {target_user.telegram_id})\n"
text += f"💰 Баланс пользователя: {settings.format_price(target_user.balance_kopeks)}\n\n"
traffic_text = "Безлимит" if (subscription.traffic_limit_gb or 0) <= 0 else f"{subscription.traffic_limit_gb} ГБ"
devices_limit = subscription.device_limit
if devices_limit is None:
devices_limit = settings.DEFAULT_DEVICE_LIMIT
devices_limit = subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT
servers_count = len(subscription.connected_squads or [])
text += f"📶 Трафик: {traffic_text}\n"
text += f"📱 Устройства: {devices_limit}\n"
@@ -3704,9 +3685,7 @@ async def admin_buy_subscription_confirm(
text += f"💰 Стоимость: {settings.format_price(price_kopeks)}\n"
text += f"💰 Баланс пользователя: {settings.format_price(target_user.balance_kopeks)}\n\n"
traffic_text = "Безлимит" if (subscription.traffic_limit_gb or 0) <= 0 else f"{subscription.traffic_limit_gb} ГБ"
devices_limit = subscription.device_limit
if devices_limit is None:
devices_limit = settings.DEFAULT_DEVICE_LIMIT
devices_limit = subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT
servers_count = len(subscription.connected_squads or [])
text += f"📶 Трафик: {traffic_text}\n"
text += f"📱 Устройства: {devices_limit}\n"
@@ -3853,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

@@ -16,10 +16,7 @@ from app.services.payment_service import PaymentService
from app.services.subscription_purchase_service import SubscriptionPurchaseService
from app.utils.decorators import error_handler
from app.states import SubscriptionStates
from app.utils.subscription_utils import (
get_display_subscription_link,
resolve_simple_subscription_device_limit,
)
from app.utils.subscription_utils import get_display_subscription_link
from app.utils.pricing_utils import compute_simple_subscription_price
logger = logging.getLogger(__name__)
@@ -38,17 +35,15 @@ async def start_simple_subscription_purchase(
if not settings.SIMPLE_SUBSCRIPTION_ENABLED:
await callback.answer("❌ Простая покупка подписки временно недоступна", show_alert=True)
return
# Проверяем, есть ли у пользователя подписка (информируем, но не блокируем покупку)
from app.database.crud.subscription import get_subscription_by_user_id
current_subscription = await get_subscription_by_user_id(db, db_user.id)
device_limit = resolve_simple_subscription_device_limit()
# Подготовим параметры простой подписки
subscription_params = {
"period_days": settings.SIMPLE_SUBSCRIPTION_PERIOD_DAYS,
"device_limit": device_limit,
"device_limit": settings.SIMPLE_SUBSCRIPTION_DEVICE_LIMIT,
"traffic_limit_gb": settings.SIMPLE_SUBSCRIPTION_TRAFFIC_GB,
"squad_uuid": settings.SIMPLE_SUBSCRIPTION_SQUAD_UUID
}
@@ -116,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}"
@@ -450,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)
@@ -648,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 = []
@@ -895,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

@@ -158,13 +158,7 @@ async def _prepare_subscription_summary(
total_servers_discount += total_discount_for_server
selected_server_prices.append(total_price_for_server)
devices_selection_enabled = settings.is_devices_selection_enabled()
if devices_selection_enabled:
devices_selected = summary_data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
else:
devices_selected = settings.get_devices_selection_disabled_amount()
summary_data['devices'] = devices_selected
devices_selected = summary_data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
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
@@ -399,14 +382,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
)
traffic_cost = settings.get_traffic_price(subscription.traffic_limit_gb)
device_limit = subscription.device_limit
if device_limit is None:
if settings.is_devices_selection_enabled():
device_limit = settings.DEFAULT_DEVICE_LIMIT
else:
device_limit = settings.get_devices_selection_disabled_amount()
devices_cost = max(0, (device_limit or 0) - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
devices_cost = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
total_cost = base_cost + servers_cost + traffic_cost + devices_cost
@@ -434,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 "Нет"
@@ -468,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

@@ -72,10 +72,9 @@ from app.utils.pricing_utils import (
apply_percentage_discount,
)
from app.utils.subscription_utils import (
convert_subscription_link_to_happ_scheme,
get_display_subscription_link,
get_happ_cryptolink_redirect_link,
resolve_simple_subscription_device_limit,
convert_subscription_link_to_happ_scheme,
)
from app.utils.promo_offer import (
build_promo_offer_hint,
@@ -141,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,
@@ -239,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 = (
@@ -273,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}
@@ -286,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,
@@ -309,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",
@@ -431,15 +415,7 @@ async def activate_trial(
return
try:
forced_devices = None
if not settings.is_devices_selection_enabled():
forced_devices = settings.get_devices_selection_disabled_amount()
subscription = await create_trial_subscription(
db,
db_user.id,
device_limit=forced_devices,
)
subscription = await create_trial_subscription(db, db_user.id)
await db.refresh(db_user)
@@ -613,13 +589,10 @@ async def start_subscription_purchase(
)
subscription = getattr(db_user, 'subscription', None)
initial_devices = settings.DEFAULT_DEVICE_LIMIT
if settings.is_devices_selection_enabled():
initial_devices = settings.DEFAULT_DEVICE_LIMIT
if subscription and getattr(subscription, 'device_limit', None) is not None:
initial_devices = max(settings.DEFAULT_DEVICE_LIMIT, subscription.device_limit)
else:
initial_devices = settings.get_devices_selection_disabled_amount()
if subscription and getattr(subscription, 'device_limit', None):
initial_devices = max(settings.DEFAULT_DEVICE_LIMIT, subscription.device_limit)
initial_data = {
'period_days': None,
@@ -725,60 +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,
)
prepared_cart_data['devices'] = settings.get_devices_selection_disabled_amount()
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
@@ -797,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(
@@ -888,14 +793,7 @@ async def handle_extend_subscription(
servers_discount_per_month = servers_price_per_month * servers_discount_percent // 100
total_servers_price = (servers_price_per_month - servers_discount_per_month) * months_in_period
device_limit = subscription.device_limit
if device_limit is None:
if settings.is_devices_selection_enabled():
device_limit = settings.DEFAULT_DEVICE_LIMIT
else:
device_limit = settings.get_devices_selection_disabled_amount()
additional_devices = max(0, (device_limit or 0) - settings.DEFAULT_DEVICE_LIMIT)
additional_devices = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT)
devices_price_per_month = additional_devices * settings.PRICE_PER_DEVICE
devices_discount_percent = db_user.get_promo_discount(
"devices",
@@ -974,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"
@@ -1071,14 +958,7 @@ async def confirm_extend_subscription(
servers_price_per_month * servers_discount_percent // 100
)
device_limit = subscription.device_limit
if device_limit is None:
if settings.is_devices_selection_enabled():
device_limit = settings.DEFAULT_DEVICE_LIMIT
else:
device_limit = settings.get_devices_selection_disabled_amount()
additional_devices = max(0, (device_limit or 0) - settings.DEFAULT_DEVICE_LIMIT)
additional_devices = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT)
devices_price_per_month = additional_devices * settings.PRICE_PER_DEVICE
devices_discount_percent = db_user.get_promo_discount(
"devices",
@@ -1368,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()
@@ -1458,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,
@@ -1554,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()
if devices_selection_enabled:
devices_selected = data.get('devices', settings.DEFAULT_DEVICE_LIMIT)
else:
devices_selected = settings.get_devices_selection_disabled_amount()
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()
@@ -1797,13 +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.get_devices_selection_disabled_amount()
should_update_devices = selected_devices is not None
was_trial_conversion = False
current_time = datetime.utcnow()
@@ -1846,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
@@ -1862,23 +1723,11 @@ async def confirm_purchase(
else:
logger.info(f"Создаем новую подписку для пользователя {db_user.telegram_id}")
default_device_limit = getattr(settings, "DEFAULT_DEVICE_LIMIT", 1)
resolved_device_limit = selected_devices
if resolved_device_limit is None:
if settings.is_devices_selection_enabled():
resolved_device_limit = default_device_limit
else:
resolved_device_limit = settings.get_devices_selection_disabled_amount()
if resolved_device_limit is None and settings.is_devices_selection_enabled():
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
)
@@ -1896,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,
@@ -1915,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(
@@ -1924,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,
@@ -1932,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(
@@ -2139,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
):
@@ -2153,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
@@ -2189,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"
@@ -2206,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),
@@ -2552,13 +2384,10 @@ async def handle_simple_subscription_purchase(
await callback.answer("❌ Простая покупка подписки временно недоступна", show_alert=True)
return
# Определяем ограничение по устройствам для текущего режима
simple_device_limit = resolve_simple_subscription_device_limit()
# Проверяем, есть ли у пользователя активная подписка
from app.database.crud.subscription import get_subscription_by_user_id
current_subscription = await get_subscription_by_user_id(db, db_user.id)
# Если у пользователя уже есть активная подписка, продлеваем её
if current_subscription and current_subscription.is_active:
# Продлеваем существующую подписку
@@ -2568,16 +2397,16 @@ async def handle_simple_subscription_purchase(
db=db,
current_subscription=current_subscription,
period_days=settings.SIMPLE_SUBSCRIPTION_PERIOD_DAYS,
device_limit=simple_device_limit,
device_limit=settings.SIMPLE_SUBSCRIPTION_DEVICE_LIMIT,
traffic_limit_gb=settings.SIMPLE_SUBSCRIPTION_TRAFFIC_GB,
squad_uuid=settings.SIMPLE_SUBSCRIPTION_SQUAD_UUID
)
return
# Подготовим параметры простой подписки
subscription_params = {
"period_days": settings.SIMPLE_SUBSCRIPTION_PERIOD_DAYS,
"device_limit": simple_device_limit,
"device_limit": settings.SIMPLE_SUBSCRIPTION_DEVICE_LIMIT,
"traffic_limit_gb": settings.SIMPLE_SUBSCRIPTION_TRAFFIC_GB,
"squad_uuid": settings.SIMPLE_SUBSCRIPTION_SQUAD_UUID
}
@@ -2612,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")],
@@ -2640,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

@@ -120,9 +120,9 @@ class AdvertisingCampaignService:
return CampaignBonusResult(success=False)
traffic_limit = campaign.subscription_traffic_gb
device_limit = campaign.subscription_device_limit
if device_limit is None:
device_limit = settings.DEFAULT_DEVICE_LIMIT
device_limit = (
campaign.subscription_device_limit or settings.DEFAULT_DEVICE_LIMIT
)
squads = list(campaign.subscription_squads or [])
if not squads:

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

@@ -131,20 +131,12 @@ class PromoCodeService:
if getattr(settings, 'TRIAL_SQUAD_UUID', None):
trial_squads = [settings.TRIAL_SQUAD_UUID]
forced_devices = None
if not settings.is_devices_selection_enabled():
forced_devices = settings.get_devices_selection_disabled_amount()
device_limit = settings.DEFAULT_DEVICE_LIMIT
if forced_devices is not None:
device_limit = forced_devices
new_subscription = await create_paid_subscription(
db=db,
user_id=user.id,
duration_days=promocode.subscription_days,
traffic_limit_gb=0,
device_limit=device_limit,
device_limit=1,
connected_squads=trial_squads,
update_server_counters=True,
)
@@ -163,15 +155,10 @@ class PromoCodeService:
if not subscription:
trial_days = promocode.subscription_days if promocode.subscription_days > 0 else settings.TRIAL_DURATION_DAYS
forced_devices = None
if not settings.is_devices_selection_enabled():
forced_devices = settings.get_devices_selection_disabled_amount()
trial_subscription = await create_trial_subscription(
db,
user.id,
duration_days=trial_days,
device_limit=forced_devices,
duration_days=trial_days
)
await self.subscription_service.create_remnawave_user(db, trial_subscription)

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(
@@ -581,14 +565,7 @@ class SubscriptionService:
servers_discount = servers_price * servers_discount_percent // 100
discounted_servers_price = servers_price - servers_discount
device_limit = subscription.device_limit
if device_limit is None:
if settings.is_devices_selection_enabled():
device_limit = settings.DEFAULT_DEVICE_LIMIT
else:
device_limit = settings.get_devices_selection_disabled_amount()
devices_price = max(0, (device_limit or 0) - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
devices_price = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
devices_discount_percent = _resolve_discount_percent(
user,
promo_group,
@@ -640,7 +617,7 @@ class SubscriptionService:
)
logger.info(message)
if devices_price > 0:
message = f" 📱 Устройства ({device_limit}): {discounted_devices_price/100}"
message = f" 📱 Устройства ({subscription.device_limit}): {discounted_devices_price/100}"
if devices_discount > 0:
message += (
f" (скидка {devices_discount_percent}%: -{devices_discount/100}₽ от {devices_price/100}₽)"
@@ -917,14 +894,7 @@ class SubscriptionService:
discounted_servers_per_month = servers_price_per_month - servers_discount_per_month
total_servers_price = discounted_servers_per_month * months_in_period
device_limit = subscription.device_limit
if device_limit is None:
if settings.is_devices_selection_enabled():
device_limit = settings.DEFAULT_DEVICE_LIMIT
else:
device_limit = settings.get_devices_selection_disabled_amount()
additional_devices = max(0, (device_limit or 0) - settings.DEFAULT_DEVICE_LIMIT)
additional_devices = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT)
devices_price_per_month = additional_devices * settings.PRICE_PER_DEVICE
devices_discount_percent = _resolve_discount_percent(
user,

View File

@@ -195,8 +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",
"DEVICES_SELECTION_DISABLED_AMOUNT": "SUBSCRIPTIONS_CORE",
"BASE_SUBSCRIPTION_PRICE": "SUBSCRIPTIONS_CORE",
"DEFAULT_TRAFFIC_RESET_STRATEGY": "TRAFFIC",
"RESET_TRAFFIC_ON_PAYMENT": "TRAFFIC",
@@ -452,21 +450,6 @@ class BotConfigurationService:
"example": "d4aa2b8c-9a36-4f31-93a2-6f07dad05fba",
"warning": "Убедитесь, что выбранный сквад активен и доступен для подписки.",
},
"DEVICES_SELECTION_ENABLED": {
"description": "Разрешает пользователям выбирать количество устройств при покупке и продлении подписки.",
"format": "Булево значение.",
"example": "false",
"warning": "При отключении пользователи не смогут докупать устройства из интерфейса бота.",
},
"DEVICES_SELECTION_DISABLED_AMOUNT": {
"description": (
"Лимит устройств, который автоматически назначается, когда выбор количества устройств выключен. "
"Значение 0 отключает назначение устройств."
),
"format": "Целое число от 0 и выше.",
"example": "3",
"warning": "При 0 RemnaWave не получит лимит устройств, пользователям не показываются цифры в интерфейсе.",
},
"CRYPTOBOT_ENABLED": {
"description": "Разрешает принимать криптоплатежи через CryptoBot.",
"format": "Булево значение.",

View File

@@ -174,29 +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():
forced_limit = settings.get_disabled_mode_device_limit()
return forced_limit
limit = getattr(subscription, "device_limit", None)
if limit is None or limit <= 0:
return None
return limit
def resolve_simple_subscription_device_limit() -> int:
"""Return the effective device limit for simple subscription flows."""
if settings.is_devices_selection_enabled():
return int(getattr(settings, "SIMPLE_SUBSCRIPTION_DEVICE_LIMIT", 0) or 0)
return settings.get_devices_selection_disabled_amount()

View File

@@ -3121,16 +3121,8 @@ async def activate_subscription_trial_endpoint(
},
)
forced_devices = None
if not settings.is_devices_selection_enabled():
forced_devices = settings.get_devices_selection_disabled_amount()
try:
subscription = await create_trial_subscription(
db,
user.id,
device_limit=forced_devices,
)
subscription = await create_trial_subscription(db, user.id)
except Exception as error: # pragma: no cover - defensive logging
logger.error(
"Failed to activate trial subscription for user %s: %s",
@@ -3646,9 +3638,7 @@ async def _calculate_subscription_renewal_pricing(
if traffic_limit is None:
traffic_limit = settings.DEFAULT_TRAFFIC_LIMIT_GB
devices_limit = subscription.device_limit
if devices_limit is None:
devices_limit = settings.DEFAULT_DEVICE_LIMIT
devices_limit = subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT
total_cost, details = await calculate_subscription_total_cost(
db,
@@ -5054,12 +5044,7 @@ async def update_subscription_devices_endpoint(
},
)
current_devices_value = subscription.device_limit
if current_devices_value is None:
fallback_value = settings.DEFAULT_DEVICE_LIMIT or 1
current_devices_value = fallback_value
current_devices = int(current_devices_value)
current_devices = int(subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT or 1)
old_devices = current_devices
if new_devices == current_devices:

View File

@@ -112,38 +112,24 @@ async def create_subscription(
if existing:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "User already has a subscription")
forced_devices = None
if not settings.is_devices_selection_enabled():
forced_devices = settings.get_devices_selection_disabled_amount()
if payload.is_trial:
trial_device_limit = payload.device_limit
if trial_device_limit is None:
trial_device_limit = forced_devices
subscription = await create_trial_subscription(
db,
user_id=payload.user_id,
duration_days=payload.duration_days,
traffic_limit_gb=payload.traffic_limit_gb,
device_limit=trial_device_limit,
device_limit=payload.device_limit,
squad_uuid=payload.squad_uuid,
)
else:
if payload.duration_days is None:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "duration_days is required for paid subscriptions")
device_limit = payload.device_limit
if device_limit is None:
if forced_devices is not None:
device_limit = forced_devices
else:
device_limit = settings.DEFAULT_DEVICE_LIMIT
subscription = await create_paid_subscription(
db,
user_id=payload.user_id,
duration_days=payload.duration_days,
traffic_limit_gb=payload.traffic_limit_gb or settings.DEFAULT_TRAFFIC_LIMIT_GB,
device_limit=device_limit,
device_limit=payload.device_limit or settings.DEFAULT_DEVICE_LIMIT,
connected_squads=payload.connected_squads or [],
update_server_counters=True,
)

View File

@@ -1,113 +0,0 @@
import pytest
from app.utils import subscription_utils
from app.utils.subscription_utils import (
resolve_hwid_device_limit,
resolve_simple_subscription_device_limit,
)
class DummySubscription:
def __init__(self, device_limit=None):
self.device_limit = device_limit
class StubSettings:
def __init__(
self,
enabled: bool,
disabled_amount,
*,
simple_limit: int = 3,
disabled_selection_amount: int = 0,
):
self._enabled = enabled
self._disabled_amount = disabled_amount
self._disabled_selection_amount = disabled_selection_amount
self.SIMPLE_SUBSCRIPTION_DEVICE_LIMIT = simple_limit
def is_devices_selection_enabled(self) -> bool:
return self._enabled
def get_disabled_mode_device_limit(self):
return self._disabled_amount
def get_devices_selection_disabled_amount(self) -> int:
return self._disabled_selection_amount
@pytest.mark.parametrize(
"forced_amount, expected",
[
(0, None),
(5, 5),
],
)
def test_resolve_hwid_device_limit_disabled_mode(monkeypatch, forced_amount, expected):
subscription = DummySubscription(device_limit=42)
monkeypatch.setattr(
subscription_utils,
"settings",
StubSettings(
enabled=False,
disabled_amount=None if forced_amount == 0 else forced_amount,
disabled_selection_amount=forced_amount,
),
)
assert resolve_hwid_device_limit(subscription) == expected
def test_resolve_hwid_device_limit_enabled_mode(monkeypatch):
subscription = DummySubscription(device_limit=4)
monkeypatch.setattr(
subscription_utils,
"settings",
StubSettings(enabled=True, disabled_amount=None),
)
assert resolve_hwid_device_limit(subscription) == 4
def test_resolve_hwid_device_limit_enabled_ignores_non_positive(monkeypatch):
subscription = DummySubscription(device_limit=0)
monkeypatch.setattr(
subscription_utils,
"settings",
StubSettings(enabled=True, disabled_amount=None),
)
assert resolve_hwid_device_limit(subscription) is None
@pytest.mark.parametrize(
"enabled, simple_limit, disabled_amount, disabled_selection_amount, expected",
[
(True, 4, None, 0, 4),
(False, 4, None, 0, 0),
(False, 4, 7, 7, 7),
],
)
def test_resolve_simple_subscription_device_limit(
monkeypatch,
enabled,
simple_limit,
disabled_amount,
disabled_selection_amount,
expected,
):
monkeypatch.setattr(
subscription_utils,
"settings",
StubSettings(
enabled=enabled,
disabled_amount=disabled_amount,
simple_limit=simple_limit,
disabled_selection_amount=disabled_selection_amount,
),
)
assert resolve_simple_subscription_device_limit() == expected

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