diff --git a/bot/handlers/user/start.py b/bot/handlers/user/start.py
index cd2d980..5df4bce 100644
--- a/bot/handlers/user/start.py
+++ b/bot/handlers/user/start.py
@@ -675,7 +675,7 @@ async def main_action_callback_handler(
if action == "subscribe":
await user_subscription_handlers.display_subscription_options(
- callback, i18n_data, settings, session)
+ callback, i18n_data, settings, session, promo_code_service=promo_code_service)
elif action == "my_subscription":
await user_subscription_handlers.my_subscription_command_handler(
callback, i18n_data, settings, panel_service, subscription_service,
diff --git a/bot/handlers/user/subscription/core.py b/bot/handlers/user/subscription/core.py
index 60555ee..2f7e4db 100644
--- a/bot/handlers/user/subscription/core.py
+++ b/bot/handlers/user/subscription/core.py
@@ -1,5 +1,6 @@
import hashlib
import logging
+import math
from aiogram import Router, F, types, Bot
from aiogram.filters import Command
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo
@@ -39,7 +40,13 @@ def _hwid_callback_token(hwid: Optional[str]) -> str:
return hashlib.sha256(hwid_str.encode()).hexdigest()[:32]
-async def display_subscription_options(event: Union[types.Message, types.CallbackQuery], i18n_data: dict, settings: Settings, session: AsyncSession):
+async def display_subscription_options(
+ event: Union[types.Message, types.CallbackQuery],
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession,
+ promo_code_service=None,
+):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
@@ -61,21 +68,46 @@ async def display_subscription_options(event: Union[types.Message, types.Callbac
stars_traffic_packages = getattr(settings, "stars_traffic_packages", {}) or {}
traffic_mode = bool(getattr(settings, "traffic_sale_mode", False) or stars_traffic_packages)
+ options_are_stars = False
if traffic_mode:
if traffic_packages:
options = traffic_packages
elif stars_traffic_packages:
options = stars_traffic_packages
currency_symbol_val = "⭐"
+ options_are_stars = True
else:
options = {}
else:
options = settings.subscription_options
- if options:
+ display_options = options
+ if options and promo_code_service:
+ try:
+ active_discount_info = await promo_code_service.get_user_active_discount(
+ session, event.from_user.id
+ )
+ except Exception:
+ active_discount_info = None
+ if active_discount_info:
+ discount_pct, _promo_code = active_discount_info
+ discounted_options = {}
+ for period, price in options.items():
+ if price is None:
+ discounted_options[period] = price
+ else:
+ discounted_price, _ = promo_code_service.calculate_discounted_price(
+ price, discount_pct
+ )
+ if options_are_stars:
+ discounted_price = math.ceil(discounted_price)
+ discounted_options[period] = discounted_price
+ display_options = discounted_options
+
+ if display_options:
text_content = get_text("select_traffic_package") if traffic_mode else get_text("select_subscription_period")
reply_markup = get_subscription_options_keyboard(
- options, currency_symbol_val, current_lang, i18n, traffic_mode=traffic_mode
+ display_options, currency_symbol_val, current_lang, i18n, traffic_mode=traffic_mode
)
else:
text_content = get_text("no_subscription_options_available")
@@ -104,8 +136,16 @@ async def display_subscription_options(event: Union[types.Message, types.Callbac
@router.callback_query(F.data == "main_action:subscribe")
-async def reshow_subscription_options_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
- await display_subscription_options(callback, i18n_data, settings, session)
+async def reshow_subscription_options_callback(
+ callback: types.CallbackQuery,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession,
+ promo_code_service=None,
+):
+ await display_subscription_options(
+ callback, i18n_data, settings, session, promo_code_service=promo_code_service
+ )
async def my_subscription_command_handler(
diff --git a/bot/handlers/user/subscription/payments_subscription.py b/bot/handlers/user/subscription/payments_subscription.py
index 5cda93d..95470dd 100644
--- a/bot/handlers/user/subscription/payments_subscription.py
+++ b/bot/handlers/user/subscription/payments_subscription.py
@@ -1,4 +1,5 @@
import logging
+import math
from typing import Optional
from aiogram import F, Router, types
@@ -52,26 +53,45 @@ async def select_subscription_period_callback_handler(
# Check for active discount and apply if exists
discount_text = ""
- if promo_code_service and price_rub:
+ if promo_code_service and (price_rub is not None or stars_price is not None):
active_discount_info = await promo_code_service.get_user_active_discount(
session, callback.from_user.id
)
if active_discount_info:
discount_pct, promo_code = active_discount_info
- original_price_rub = price_rub
- price_rub, discount_amt = promo_code_service.calculate_discounted_price(
- price_rub, discount_pct
- )
- discount_text = get_text(
- "active_discount_notice",
- code=promo_code,
- discount_pct=discount_pct,
- original_price=original_price_rub,
- discounted_price=price_rub,
- discount_amount=discount_amt
- )
- # Note: Stars prices typically don't get discounts (can be added if needed)
+ if price_rub is not None:
+ original_price_rub = price_rub
+ price_rub, discount_amt = promo_code_service.calculate_discounted_price(
+ price_rub, discount_pct
+ )
+ discount_text = get_text(
+ "active_discount_notice",
+ code=promo_code,
+ discount_pct=discount_pct,
+ original_price=original_price_rub,
+ discounted_price=price_rub,
+ discount_amount=discount_amt,
+ currency_symbol=currency_symbol_val,
+ )
+ if stars_price is not None:
+ original_stars_price = stars_price
+ discounted_stars_price, _ = promo_code_service.calculate_discounted_price(
+ float(stars_price), discount_pct
+ )
+ discounted_stars_price = math.ceil(discounted_stars_price)
+ stars_price = discounted_stars_price
+ if not discount_text:
+ discount_amt = original_stars_price - discounted_stars_price
+ discount_text = get_text(
+ "active_discount_notice",
+ code=promo_code,
+ discount_pct=discount_pct,
+ original_price=original_stars_price,
+ discounted_price=discounted_stars_price,
+ discount_amount=discount_amt,
+ currency_symbol="⭐",
+ )
if price_rub is None:
if traffic_mode and not price_source and stars_price is not None:
diff --git a/locales/en.json b/locales/en.json
index d6a3b9f..65298d5 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -79,7 +79,7 @@
"discount_promo_code_applied_success": "✅ Promo code {code} activated!\n\n💰 A {discount}% discount will be applied to your next purchase.\n\nSelect a plan for payment.",
"discount_promo_already_active": "❌ You already have an active discount promo code ({code}, -{discount_pct}%). Use it first or wait until the payment is complete.",
"promo_code_not_found_or_not_discount": "❌ Promo code {code} not found or is invalid.",
- "active_discount_notice": "🎁 Active discount: {code} (-{discount_pct}%)\n💵 Price: {original_price} ➔ {discounted_price}\n💰 Savings: {discount_amount}",
+ "active_discount_notice": "🎁 Active discount: {code} (-{discount_pct}%)\n💵 Price: {original_price}{currency_symbol} ➔ {discounted_price}{currency_symbol}\n💰 Savings: {discount_amount}{currency_symbol}",
"error_applying_promo_bonus": "Failed to apply promo bonus. Please try again later or contact support.",
"promo_input_cancelled_short": "Promo code entry cancelled.",
"trial_feature_disabled": "Free trial is currently unavailable.",
diff --git a/locales/ru.json b/locales/ru.json
index a9de522..6d19cf1 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -79,7 +79,7 @@
"discount_promo_code_applied_success": "✅ Промокод {code} активирован!\n\n💰 Скидка {discount}% будет применена к вашей следующей покупке.\n\nВыберите тариф для оплаты.",
"discount_promo_already_active": "❌ У вас уже есть активированный промокод на скидку ({code}, -{discount_pct}%). Используйте его сначала или дождитесь окончания платежа.",
"promo_code_not_found_or_not_discount": "❌ Промокод {code} не найден или недействителен.",
- "active_discount_notice": "🎁 Активна скидка: {code} (-{discount_pct}%)\n💵 Цена: {original_price} ➔ {discounted_price}\n💰 Экономия: {discount_amount}",
+ "active_discount_notice": "🎁 Активна скидка: {code} (-{discount_pct}%)\n💵 Цена: {original_price}{currency_symbol} ➔ {discounted_price}{currency_symbol}\n💰 Экономия: {discount_amount}{currency_symbol}",
"error_applying_promo_bonus": "Не удалось применить бонус по промокоду. Пожалуйста, попробуйте позже или свяжитесь с поддержкой.",
"error_applying_promo_discount": "❌ Ошибка при активации промокода. Попробуйте позже.",
"promo_input_cancelled_short": "Ввод промокода отменен.",