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": "Ввод промокода отменен.",