feat: tariff switch direction control, fix device pricing within tariff limit
Tariff switch direction: - Add TARIFF_SWITCH_UPGRADE_ENABLED and TARIFF_SWITCH_DOWNGRADE_ENABLED settings to control allowed switch directions - Guard all 10 entry points: instant switch (list, preview, confirm), legacy switch (list, select, confirm, daily confirm), cabinet (preview, execute), purchase-options API - Filter tariff lists by allowed direction, show "unavailable" when both directions disabled - Expose settings in cabinet purchase-options response for frontend Device pricing fix: - Devices within tariff.device_limit are now free when restoring (was charging for all devices regardless of tariff inclusion) - Fix max(100, price) minimum enforcing 1 RUB even when chargeable_devices is 0 - Apply fix across all endpoints: bot handlers (confirm_change, execute_change, confirm_add), cabinet API (legacy purchase, modern purchase, get-price, save-cart), inline keyboard display
This commit is contained in:
@@ -254,6 +254,15 @@ WEBHOOK_NOTIFY_DEVICES=true
|
||||
# - Подходит для продажи готовых пакетов услуг
|
||||
SALES_MODE=tariffs
|
||||
|
||||
# Управление сменой тарифа (для SALES_MODE=tariffs)
|
||||
# UPGRADE / DOWNGRADE:
|
||||
# true / true = все направления разрешены
|
||||
# true / false = только повышение (на более дорогой тариф)
|
||||
# false / true = только понижение (на более дешёвый тариф)
|
||||
# false / false = смена тарифа полностью отключена
|
||||
TARIFF_SWITCH_UPGRADE_ENABLED=true
|
||||
TARIFF_SWITCH_DOWNGRADE_ENABLED=true
|
||||
|
||||
# ===== ТРИАЛ ПОДПИСКА =====
|
||||
TRIAL_DURATION_DAYS=3
|
||||
TRIAL_TRAFFIC_LIMIT_GB=10
|
||||
|
||||
@@ -108,7 +108,24 @@ async def purchase_devices_legacy(
|
||||
detail='Докупка устройств недоступна',
|
||||
)
|
||||
|
||||
base_total_price = device_price * request.devices
|
||||
# Устройства в пределах тарифного лимита — бесплатные
|
||||
current_devices = subscription.device_limit or 1
|
||||
if tariff:
|
||||
tariff_included = tariff.device_limit or 0
|
||||
if current_devices < tariff_included:
|
||||
free_devices = tariff_included - current_devices
|
||||
chargeable_devices = max(0, request.devices - free_devices)
|
||||
else:
|
||||
chargeable_devices = request.devices
|
||||
else:
|
||||
free_baseline = settings.DEFAULT_DEVICE_LIMIT
|
||||
if current_devices < free_baseline:
|
||||
free_devices = free_baseline - current_devices
|
||||
chargeable_devices = max(0, request.devices - free_devices)
|
||||
else:
|
||||
chargeable_devices = request.devices
|
||||
|
||||
base_total_price = device_price * chargeable_devices
|
||||
|
||||
# Lock user row to prevent TOCTOU on promo-offer state
|
||||
from app.database.crud.user import lock_user_for_pricing
|
||||
@@ -366,10 +383,27 @@ async def purchase_devices(
|
||||
days_left = max(1, (end_date - now).days)
|
||||
total_days = 30 # Base period for device price calculation
|
||||
|
||||
# Устройства в пределах тарифного лимита — бесплатные
|
||||
if tariff:
|
||||
tariff_included = tariff.device_limit or 0
|
||||
if current_devices < tariff_included:
|
||||
free_devices = tariff_included - current_devices
|
||||
chargeable_devices = max(0, request.devices - free_devices)
|
||||
else:
|
||||
chargeable_devices = request.devices
|
||||
else:
|
||||
free_baseline = settings.DEFAULT_DEVICE_LIMIT
|
||||
if current_devices < free_baseline:
|
||||
free_devices = free_baseline - current_devices
|
||||
chargeable_devices = max(0, request.devices - free_devices)
|
||||
else:
|
||||
chargeable_devices = request.devices
|
||||
|
||||
# Calculate base price before discount
|
||||
base_price_per_month = device_price * request.devices
|
||||
base_price_per_month = device_price * chargeable_devices
|
||||
base_price_prorated = int(base_price_per_month * days_left / total_days)
|
||||
base_price_prorated = max(100, base_price_prorated) # Minimum 1 ruble
|
||||
if chargeable_devices > 0:
|
||||
base_price_prorated = max(100, base_price_prorated) # Minimum 1 ruble
|
||||
|
||||
# Lock user BEFORE discount computation to prevent TOCTOU on promo group
|
||||
from app.database.crud.user import lock_user_for_pricing
|
||||
@@ -627,8 +661,25 @@ async def save_devices_cart(
|
||||
days_left = max(1, (end_date - now).days)
|
||||
total_days = 30
|
||||
|
||||
base_total_price = int(device_price * request.devices * days_left / total_days)
|
||||
base_total_price = max(100, base_total_price) # Minimum 1 ruble
|
||||
# Устройства в пределах тарифного лимита — бесплатные
|
||||
if tariff:
|
||||
tariff_included = tariff.device_limit or 0
|
||||
if current_devices < tariff_included:
|
||||
free_devices = tariff_included - current_devices
|
||||
chargeable_devices = max(0, request.devices - free_devices)
|
||||
else:
|
||||
chargeable_devices = request.devices
|
||||
else:
|
||||
free_baseline = settings.DEFAULT_DEVICE_LIMIT
|
||||
if current_devices < free_baseline:
|
||||
free_devices = free_baseline - current_devices
|
||||
chargeable_devices = max(0, request.devices - free_devices)
|
||||
else:
|
||||
chargeable_devices = request.devices
|
||||
|
||||
base_total_price = int(device_price * chargeable_devices * days_left / total_days)
|
||||
if chargeable_devices > 0:
|
||||
base_total_price = max(100, base_total_price) # Minimum 1 ruble
|
||||
|
||||
# Apply discount from promo group
|
||||
period_hint_days = days_left
|
||||
@@ -724,9 +775,26 @@ async def get_device_price(
|
||||
days_left = max(1, (end_date - now).days)
|
||||
total_days = 30
|
||||
|
||||
# Устройства в пределах тарифного лимита — бесплатные
|
||||
if tariff:
|
||||
tariff_included = tariff.device_limit or 0
|
||||
if current_devices < tariff_included:
|
||||
free_devices = tariff_included - current_devices
|
||||
chargeable_devices = max(0, devices - free_devices)
|
||||
else:
|
||||
chargeable_devices = devices
|
||||
else:
|
||||
free_baseline = settings.DEFAULT_DEVICE_LIMIT
|
||||
if current_devices < free_baseline:
|
||||
free_devices = free_baseline - current_devices
|
||||
chargeable_devices = max(0, devices - free_devices)
|
||||
else:
|
||||
chargeable_devices = devices
|
||||
|
||||
# Calculate base price before discount (total first, then floor)
|
||||
base_total_price = int(device_price * devices * days_left / total_days)
|
||||
base_total_price = max(100, base_total_price)
|
||||
base_total_price = int(device_price * chargeable_devices * days_left / total_days)
|
||||
if chargeable_devices > 0:
|
||||
base_total_price = max(100, base_total_price)
|
||||
|
||||
# Apply discount from promo group
|
||||
period_hint_days = days_left
|
||||
|
||||
@@ -353,6 +353,9 @@ async def get_purchase_options(
|
||||
'all_tariffs_purchased': len(purchased_tariff_ids) >= len(tariffs)
|
||||
if settings.is_multi_tariff_enabled()
|
||||
else False,
|
||||
# Направления смены тарифа
|
||||
'tariff_switch_upgrade_enabled': settings.TARIFF_SWITCH_UPGRADE_ENABLED,
|
||||
'tariff_switch_downgrade_enabled': settings.TARIFF_SWITCH_DOWNGRADE_ENABLED,
|
||||
}
|
||||
|
||||
# Classic mode - return periods
|
||||
|
||||
@@ -119,6 +119,18 @@ async def preview_tariff_switch(
|
||||
)
|
||||
upgrade_cost = switch_result.upgrade_cost
|
||||
is_upgrade = switch_result.is_upgrade
|
||||
|
||||
# Проверяем разрешение на смену в данном направлении
|
||||
if is_upgrade and not settings.TARIFF_SWITCH_UPGRADE_ENABLED:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail='Повышение тарифа недоступно',
|
||||
)
|
||||
if not is_upgrade and not settings.TARIFF_SWITCH_DOWNGRADE_ENABLED:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail='Понижение тарифа недоступно',
|
||||
)
|
||||
base_upgrade_cost = switch_result.raw_cost
|
||||
discount_value = switch_result.discount_value
|
||||
period_discount_percent = switch_result.effective_discount_pct
|
||||
@@ -263,11 +275,24 @@ async def switch_tariff(
|
||||
user=user,
|
||||
)
|
||||
upgrade_cost = switch_result.upgrade_cost
|
||||
is_upgrade = switch_result.is_upgrade
|
||||
base_upgrade_cost = switch_result.raw_cost
|
||||
discount_value = switch_result.discount_value
|
||||
period_discount_percent = switch_result.effective_discount_pct
|
||||
new_period_days = switch_result.new_period_days
|
||||
|
||||
# Проверяем разрешение на смену в данном направлении
|
||||
if is_upgrade and not settings.TARIFF_SWITCH_UPGRADE_ENABLED:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail='Повышение тарифа недоступно',
|
||||
)
|
||||
if not is_upgrade and not settings.TARIFF_SWITCH_DOWNGRADE_ENABLED:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail='Понижение тарифа недоступно',
|
||||
)
|
||||
|
||||
# Validate daily price for switching TO daily
|
||||
new_is_daily = getattr(new_tariff, 'is_daily', False)
|
||||
current_is_daily = getattr(current_tariff, 'is_daily', False) if current_tariff else False
|
||||
|
||||
@@ -148,6 +148,8 @@ class Settings(BaseSettings):
|
||||
DEFAULT_TRAFFIC_RESET_STRATEGY: str = 'MONTH'
|
||||
RESET_TRAFFIC_ON_PAYMENT: bool = False
|
||||
RESET_TRAFFIC_ON_TARIFF_SWITCH: bool = True
|
||||
TARIFF_SWITCH_UPGRADE_ENABLED: bool = True
|
||||
TARIFF_SWITCH_DOWNGRADE_ENABLED: bool = True
|
||||
MAX_DEVICES_LIMIT: int = 20
|
||||
|
||||
TRIAL_WARNING_HOURS: int = 2
|
||||
|
||||
@@ -325,9 +325,14 @@ async def confirm_change_devices(
|
||||
if devices_difference > 0:
|
||||
additional_devices = devices_difference
|
||||
|
||||
# Для тарифов - все устройства платные (нет бесплатного лимита)
|
||||
# Устройства в пределах тарифного лимита — бесплатные
|
||||
if tariff:
|
||||
chargeable_devices = additional_devices
|
||||
tariff_included = tariff.device_limit or 0
|
||||
if current_devices < tariff_included:
|
||||
free_devices = tariff_included - current_devices
|
||||
chargeable_devices = max(0, additional_devices - free_devices)
|
||||
else:
|
||||
chargeable_devices = additional_devices
|
||||
elif current_devices < settings.DEFAULT_DEVICE_LIMIT:
|
||||
free_devices = settings.DEFAULT_DEVICE_LIMIT - current_devices
|
||||
chargeable_devices = max(0, additional_devices - free_devices)
|
||||
@@ -352,7 +357,8 @@ async def confirm_change_devices(
|
||||
)
|
||||
# Цена = месячная_цена * days_left / 30
|
||||
price = int(discounted_per_month * days_left / 30)
|
||||
price = max(100, price) # Минимум 1 рубль
|
||||
if chargeable_devices > 0:
|
||||
price = max(100, price) # Минимум 1 рубль (только для платных устройств)
|
||||
total_discount = int(discount_per_month * days_left / 30)
|
||||
period_label = f'{days_left} дн.' if days_left > 1 else '1 день'
|
||||
|
||||
@@ -553,7 +559,12 @@ async def execute_change_devices(
|
||||
devices_difference = new_devices_count - current_devices
|
||||
if devices_difference > 0:
|
||||
if tariff:
|
||||
chargeable_devices = devices_difference
|
||||
tariff_included = tariff.device_limit or 0
|
||||
if current_devices < tariff_included:
|
||||
free_devices = tariff_included - current_devices
|
||||
chargeable_devices = max(0, devices_difference - free_devices)
|
||||
else:
|
||||
chargeable_devices = devices_difference
|
||||
elif current_devices < settings.DEFAULT_DEVICE_LIMIT:
|
||||
free_devices = settings.DEFAULT_DEVICE_LIMIT - current_devices
|
||||
chargeable_devices = max(0, devices_difference - free_devices)
|
||||
@@ -572,7 +583,8 @@ async def execute_change_devices(
|
||||
devices_discount_percent,
|
||||
)
|
||||
price = int(discounted_per_month * days_left / 30)
|
||||
price = max(100, price)
|
||||
if chargeable_devices > 0:
|
||||
price = max(100, price)
|
||||
else:
|
||||
price = 0
|
||||
|
||||
@@ -1215,7 +1227,22 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db:
|
||||
)
|
||||
return
|
||||
|
||||
devices_price_per_month = devices_count * price_per_device
|
||||
# Устройства в пределах тарифного лимита — бесплатные
|
||||
current_devices = subscription.device_limit or 1
|
||||
if tariff:
|
||||
tariff_included = tariff.device_limit or 0
|
||||
if current_devices < tariff_included:
|
||||
free_devices = tariff_included - current_devices
|
||||
chargeable_devices = max(0, devices_count - free_devices)
|
||||
else:
|
||||
chargeable_devices = devices_count
|
||||
elif current_devices < settings.DEFAULT_DEVICE_LIMIT:
|
||||
free_devices = settings.DEFAULT_DEVICE_LIMIT - current_devices
|
||||
chargeable_devices = max(0, devices_count - free_devices)
|
||||
else:
|
||||
chargeable_devices = devices_count
|
||||
|
||||
devices_price_per_month = chargeable_devices * price_per_device
|
||||
|
||||
# TOCTOU: lock user row before reading promo/discount state
|
||||
db_user = await lock_user_for_pricing(db, db_user.id)
|
||||
@@ -1240,7 +1267,8 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db:
|
||||
)
|
||||
# Цена = месячная_цена * days_left / 30
|
||||
price = int(discounted_per_month * days_left / 30)
|
||||
price = max(100, price) # Минимум 1 рубль
|
||||
if chargeable_devices > 0:
|
||||
price = max(100, price) # Минимум 1 рубль (только для платных устройств)
|
||||
total_discount = int(discount_per_month * days_left / 30)
|
||||
period_label = f'{days_left} дн.' if days_left > 1 else '1 день'
|
||||
else:
|
||||
@@ -1260,7 +1288,8 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db:
|
||||
)
|
||||
# Цена = месячная_цена * days_left / 30
|
||||
price = int(discounted_per_month * days_left / 30)
|
||||
price = max(100, price) # Минимум 1 рубль
|
||||
if chargeable_devices > 0:
|
||||
price = max(100, price) # Минимум 1 рубль (только для платных устройств)
|
||||
total_discount = int(discount_per_month * days_left / 30)
|
||||
period_label = f'{days_left} дн.' if days_left > 1 else '1 день'
|
||||
|
||||
|
||||
@@ -2666,6 +2666,18 @@ async def show_tariff_switch_list(
|
||||
|
||||
current_tariff_id = subscription.tariff_id
|
||||
|
||||
# Проверяем, разрешена ли смена тарифа хотя бы в одном направлении
|
||||
if not settings.TARIFF_SWITCH_UPGRADE_ENABLED and not settings.TARIFF_SWITCH_DOWNGRADE_ENABLED:
|
||||
await callback.message.edit_text(
|
||||
'🚫 <b>Смена тарифа недоступна</b>\n\nАдминистратор отключил возможность смены тарифа.',
|
||||
reply_markup=InlineKeyboardMarkup(
|
||||
inline_keyboard=[[InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')]]
|
||||
),
|
||||
parse_mode='HTML',
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
# Получаем доступные тарифы
|
||||
promo_group_id = getattr(db_user, 'promo_group_id', None)
|
||||
tariffs = await get_tariffs_for_user(db, promo_group_id)
|
||||
@@ -2678,6 +2690,14 @@ async def show_tariff_switch_list(
|
||||
else:
|
||||
available_tariffs = [t for t in tariffs if t.id != current_tariff_id]
|
||||
|
||||
# Фильтруем по разрешённым направлениям (upgrade/downgrade)
|
||||
current_tariff = await get_tariff_by_id(db, current_tariff_id) if current_tariff_id else None
|
||||
if current_tariff:
|
||||
remaining_days = max(0, (subscription.end_date - datetime.now(UTC)).days) if subscription.end_date else 0
|
||||
available_tariffs = _filter_tariffs_by_switch_direction(
|
||||
available_tariffs, current_tariff, remaining_days, db_user
|
||||
)
|
||||
|
||||
if not available_tariffs:
|
||||
await callback.message.edit_text(
|
||||
'😔 <b>Нет доступных тарифов для переключения</b>\n\nВы уже используете единственный доступный тариф.',
|
||||
@@ -2739,6 +2759,24 @@ async def select_tariff_switch(
|
||||
await callback.answer('Тариф недоступен', show_alert=True)
|
||||
return
|
||||
|
||||
# Проверяем разрешение на смену в данном направлении
|
||||
current_subscription_sw, _sw_sub_id_check = await _resolve_subscription(callback, db_user, db, state)
|
||||
if current_subscription_sw and current_subscription_sw.tariff_id:
|
||||
cur_tariff_sw = await get_tariff_by_id(db, current_subscription_sw.tariff_id)
|
||||
if cur_tariff_sw:
|
||||
rem_days = (
|
||||
max(0, (current_subscription_sw.end_date - datetime.now(UTC)).days)
|
||||
if current_subscription_sw.end_date
|
||||
else 0
|
||||
)
|
||||
_, is_up = _calculate_instant_switch_cost(cur_tariff_sw, tariff, rem_days, db_user)
|
||||
if is_up and not settings.TARIFF_SWITCH_UPGRADE_ENABLED:
|
||||
await callback.answer('Повышение тарифа недоступно', show_alert=True)
|
||||
return
|
||||
if not is_up and not settings.TARIFF_SWITCH_DOWNGRADE_ENABLED:
|
||||
await callback.answer('Понижение тарифа недоступно', show_alert=True)
|
||||
return
|
||||
|
||||
traffic = format_traffic(tariff.traffic_limit_gb)
|
||||
|
||||
# Проверяем, суточный ли это тариф
|
||||
@@ -2960,6 +2998,19 @@ async def confirm_tariff_switch(
|
||||
await callback.answer('У вас нет активной подписки', show_alert=True)
|
||||
return
|
||||
|
||||
# Проверяем разрешение на смену в данном направлении
|
||||
if subscription.tariff_id and subscription.tariff_id != tariff_id:
|
||||
cur_tariff_obj = await get_tariff_by_id(db, subscription.tariff_id)
|
||||
if cur_tariff_obj:
|
||||
rem_days = max(0, (subscription.end_date - datetime.now(UTC)).days) if subscription.end_date else 0
|
||||
_, is_up = _calculate_instant_switch_cost(cur_tariff_obj, tariff, rem_days, db_user)
|
||||
if is_up and not settings.TARIFF_SWITCH_UPGRADE_ENABLED:
|
||||
await callback.answer('Повышение тарифа недоступно', show_alert=True)
|
||||
return
|
||||
if not is_up and not settings.TARIFF_SWITCH_DOWNGRADE_ENABLED:
|
||||
await callback.answer('Понижение тарифа недоступно', show_alert=True)
|
||||
return
|
||||
|
||||
# Calculate price via PricingEngine (handles per-category discounts + extra devices)
|
||||
from app.services.pricing_engine import pricing_engine
|
||||
|
||||
@@ -3212,6 +3263,19 @@ async def confirm_daily_tariff_switch(
|
||||
await callback.answer('У вас нет активной подписки', show_alert=True)
|
||||
return
|
||||
|
||||
# Проверяем разрешение на смену в данном направлении
|
||||
if subscription.tariff_id and subscription.tariff_id != tariff_id:
|
||||
cur_tariff_daily = await get_tariff_by_id(db, subscription.tariff_id)
|
||||
if cur_tariff_daily:
|
||||
rem_days = max(0, (subscription.end_date - datetime.now(UTC)).days) if subscription.end_date else 0
|
||||
_, is_up = _calculate_instant_switch_cost(cur_tariff_daily, tariff, rem_days, db_user)
|
||||
if is_up and not settings.TARIFF_SWITCH_UPGRADE_ENABLED:
|
||||
await callback.answer('Повышение тарифа недоступно', show_alert=True)
|
||||
return
|
||||
if not is_up and not settings.TARIFF_SWITCH_DOWNGRADE_ENABLED:
|
||||
await callback.answer('Понижение тарифа недоступно', show_alert=True)
|
||||
return
|
||||
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
try:
|
||||
@@ -3443,6 +3507,30 @@ def _calculate_instant_switch_cost(
|
||||
return result.upgrade_cost, result.is_upgrade
|
||||
|
||||
|
||||
def _filter_tariffs_by_switch_direction(
|
||||
tariffs: list[Tariff],
|
||||
current_tariff: Tariff,
|
||||
remaining_days: int,
|
||||
db_user: User | None = None,
|
||||
) -> list[Tariff]:
|
||||
"""Фильтрует тарифы по разрешённым направлениям смены (upgrade/downgrade)."""
|
||||
upgrade_ok = settings.TARIFF_SWITCH_UPGRADE_ENABLED
|
||||
downgrade_ok = settings.TARIFF_SWITCH_DOWNGRADE_ENABLED
|
||||
|
||||
if upgrade_ok and downgrade_ok:
|
||||
return tariffs
|
||||
|
||||
filtered = []
|
||||
for tariff in tariffs:
|
||||
if tariff.id == current_tariff.id:
|
||||
filtered.append(tariff)
|
||||
continue
|
||||
_, is_upgrade = _calculate_instant_switch_cost(current_tariff, tariff, remaining_days, db_user)
|
||||
if (is_upgrade and upgrade_ok) or (not is_upgrade and downgrade_ok):
|
||||
filtered.append(tariff)
|
||||
return filtered
|
||||
|
||||
|
||||
def format_instant_switch_list_text(
|
||||
tariffs: list[Tariff],
|
||||
current_tariff: Tariff,
|
||||
@@ -3450,16 +3538,21 @@ def format_instant_switch_list_text(
|
||||
db_user: User | None = None,
|
||||
) -> str:
|
||||
"""Форматирует текст со списком тарифов для мгновенного переключения."""
|
||||
upgrade_ok = settings.TARIFF_SWITCH_UPGRADE_ENABLED
|
||||
downgrade_ok = settings.TARIFF_SWITCH_DOWNGRADE_ENABLED
|
||||
|
||||
lines = [
|
||||
'📦 <b>Мгновенная смена тарифа</b>',
|
||||
f'📌 Текущий: <b>{html.escape(current_tariff.name)}</b>',
|
||||
f'⏰ Осталось: <b>{remaining_days} дн.</b>',
|
||||
'',
|
||||
'💡 При переключении остаток дней сохраняется.',
|
||||
'⬆️ Повышение тарифа = доплата за разницу',
|
||||
'⬇️ Понижение = бесплатно',
|
||||
'',
|
||||
]
|
||||
if upgrade_ok:
|
||||
lines.append('⬆️ Повышение тарифа = доплата за разницу')
|
||||
if downgrade_ok:
|
||||
lines.append('⬇️ Понижение = бесплатно')
|
||||
lines.append('')
|
||||
|
||||
for tariff in tariffs:
|
||||
if tariff.id == current_tariff.id:
|
||||
@@ -3591,6 +3684,18 @@ async def show_instant_switch_list(
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
# Проверяем, разрешена ли смена тарифа хотя бы в одном направлении
|
||||
if not settings.TARIFF_SWITCH_UPGRADE_ENABLED and not settings.TARIFF_SWITCH_DOWNGRADE_ENABLED:
|
||||
await callback.message.edit_text(
|
||||
'🚫 <b>Смена тарифа недоступна</b>\n\nАдминистратор отключил возможность смены тарифа.',
|
||||
reply_markup=InlineKeyboardMarkup(
|
||||
inline_keyboard=[[InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')]]
|
||||
),
|
||||
parse_mode='HTML',
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
# Получаем доступные тарифы
|
||||
promo_group_id = getattr(db_user, 'promo_group_id', None)
|
||||
tariffs = await get_tariffs_for_user(db, promo_group_id)
|
||||
@@ -3603,6 +3708,9 @@ async def show_instant_switch_list(
|
||||
else:
|
||||
available_tariffs = [t for t in tariffs if t.id != current_tariff.id]
|
||||
|
||||
# Фильтруем по разрешённым направлениям (upgrade/downgrade)
|
||||
available_tariffs = _filter_tariffs_by_switch_direction(available_tariffs, current_tariff, remaining_days, db_user)
|
||||
|
||||
if not available_tariffs:
|
||||
await callback.message.edit_text(
|
||||
'😔 <b>Нет доступных тарифов для переключения</b>\n\nВы уже используете единственный доступный тариф.',
|
||||
@@ -3672,6 +3780,14 @@ async def preview_instant_switch(
|
||||
# Рассчитываем стоимость переключения
|
||||
upgrade_cost, is_upgrade = _calculate_instant_switch_cost(current_tariff, new_tariff, remaining_days, db_user)
|
||||
|
||||
# Проверяем разрешение на смену в данном направлении
|
||||
if is_upgrade and not settings.TARIFF_SWITCH_UPGRADE_ENABLED:
|
||||
await callback.answer('Повышение тарифа недоступно', show_alert=True)
|
||||
return
|
||||
if not is_upgrade and not settings.TARIFF_SWITCH_DOWNGRADE_ENABLED:
|
||||
await callback.answer('Понижение тарифа недоступно', show_alert=True)
|
||||
return
|
||||
|
||||
# Проверяем баланс
|
||||
user_balance = db_user.balance_kopeks or 0
|
||||
|
||||
@@ -3846,6 +3962,14 @@ async def confirm_instant_switch(
|
||||
is_upgrade = switch_result.is_upgrade
|
||||
consume_promo = switch_result.offer_discount_pct > 0
|
||||
|
||||
# Проверяем разрешение на смену в данном направлении
|
||||
if is_upgrade and not settings.TARIFF_SWITCH_UPGRADE_ENABLED:
|
||||
await callback.answer('Повышение тарифа недоступно', show_alert=True)
|
||||
return
|
||||
if not is_upgrade and not settings.TARIFF_SWITCH_DOWNGRADE_ENABLED:
|
||||
await callback.answer('Понижение тарифа недоступно', show_alert=True)
|
||||
return
|
||||
|
||||
# Проверяем баланс если это upgrade (use locked user's fresh balance)
|
||||
user_balance = db_user.balance_kopeks or 0
|
||||
if is_upgrade and user_balance < upgrade_cost:
|
||||
|
||||
@@ -2310,8 +2310,8 @@ def get_change_devices_keyboard(
|
||||
tariff_device_price = getattr(tariff, 'device_price_kopeks', None) if tariff else None
|
||||
if tariff and tariff_device_price:
|
||||
device_price_per_month = tariff_device_price
|
||||
# Для тарифов все устройства платные (нет бесплатного лимита)
|
||||
default_device_limit = 0
|
||||
# Устройства в пределах тарифного лимита — бесплатные
|
||||
default_device_limit = tariff.device_limit if tariff else 0
|
||||
else:
|
||||
device_price_per_month = settings.PRICE_PER_DEVICE
|
||||
default_device_limit = settings.DEFAULT_DEVICE_LIMIT
|
||||
|
||||
Reference in New Issue
Block a user