diff --git a/app/database/crud/discount_offer.py b/app/database/crud/discount_offer.py index 0ae6a068..d3ee75a0 100644 --- a/app/database/crud/discount_offer.py +++ b/app/database/crud/discount_offer.py @@ -16,7 +16,7 @@ async def upsert_discount_offer( discount_percent: int, bonus_amount_kopeks: int, valid_hours: int, - effect_type: str = "balance_bonus", + effect_type: str = "percent_discount", extra_data: Optional[dict] = None, ) -> DiscountOffer: """Create or refresh a discount offer for a user.""" @@ -67,14 +67,50 @@ async def get_offer_by_id(db: AsyncSession, offer_id: int) -> Optional[DiscountO return result.scalar_one_or_none() -async def mark_offer_claimed(db: AsyncSession, offer: DiscountOffer) -> DiscountOffer: - offer.claimed_at = datetime.utcnow() - offer.is_active = False +async def mark_offer_claimed( + db: AsyncSession, + offer: DiscountOffer, + *, + deactivate: bool = True, +) -> DiscountOffer: + now = datetime.utcnow() + offer.claimed_at = now + if deactivate: + offer.is_active = False + offer.consumed_at = now await db.commit() await db.refresh(offer) return offer +async def mark_offer_consumed(db: AsyncSession, offer: DiscountOffer) -> DiscountOffer: + offer.is_active = False + offer.consumed_at = datetime.utcnow() + await db.commit() + await db.refresh(offer) + return offer + + +async def get_active_percent_discount_offer( + db: AsyncSession, + user_id: int, +) -> Optional[DiscountOffer]: + now = datetime.utcnow() + result = await db.execute( + select(DiscountOffer) + .where( + DiscountOffer.user_id == user_id, + DiscountOffer.effect_type == "percent_discount", + DiscountOffer.is_active == True, # noqa: E712 + DiscountOffer.expires_at > now, + DiscountOffer.claimed_at.isnot(None), + DiscountOffer.consumed_at.is_(None), + ) + .order_by(DiscountOffer.discount_percent.desc(), DiscountOffer.expires_at.desc()) + ) + return result.scalars().first() + + async def deactivate_expired_offers(db: AsyncSession) -> int: now = datetime.utcnow() result = await db.execute( diff --git a/app/database/crud/promo_offer_template.py b/app/database/crud/promo_offer_template.py index cb99770b..40754eb1 100644 --- a/app/database/crud/promo_offer_template.py +++ b/app/database/crud/promo_offer_template.py @@ -31,8 +31,8 @@ DEFAULT_TEMPLATES: tuple[dict, ...] = ( "name": "Скидка на продление", "message_text": ( "💎 Экономия {discount_percent}% при продлении\n\n" - "Мы начислим {bonus_amount} на баланс после активации, чтобы продление обошлось дешевле.\n" - "Срок действия предложения — {valid_hours} ч." + "Активируйте предложение, и скидка {discount_percent}% автоматически применится при оплате.\n" + "Экономия до {bonus_amount}. Скидка действует {valid_hours} ч и суммируется с вашей промогруппой." ), "button_text": "🎁 Получить скидку", "valid_hours": 24, @@ -46,8 +46,8 @@ DEFAULT_TEMPLATES: tuple[dict, ...] = ( "name": "Скидка на покупку", "message_text": ( "🎯 Вернитесь со скидкой {discount_percent}%\n\n" - "Начислим {bonus_amount} после активации — используйте бонус при оплате новой подписки.\n" - "Предложение действует {valid_hours} ч." + "Скидка {discount_percent}% автоматически применится при покупке новой подписки.\n" + "Экономия до {bonus_amount}. Предложение действует {valid_hours} ч и суммируется с вашей промогруппой." ), "button_text": "🎁 Забрать скидку", "valid_hours": 48, diff --git a/app/database/models.py b/app/database/models.py index 6de613e4..b18b5279 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -811,8 +811,9 @@ class DiscountOffer(Base): bonus_amount_kopeks = Column(Integer, nullable=False, default=0) expires_at = Column(DateTime, nullable=False) claimed_at = Column(DateTime, nullable=True) + consumed_at = Column(DateTime, nullable=True) is_active = Column(Boolean, default=True, nullable=False) - effect_type = Column(String(50), nullable=False, default="balance_bonus") + effect_type = Column(String(50), nullable=False, default="percent_discount") extra_data = Column(JSON, nullable=True) created_at = Column(DateTime, default=func.now()) updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py index 4d5dd201..bb0a77ac 100644 --- a/app/database/universal_migration.py +++ b/app/database/universal_migration.py @@ -727,8 +727,9 @@ async def create_discount_offers_table(): bonus_amount_kopeks INTEGER NOT NULL DEFAULT 0, expires_at DATETIME NOT NULL, claimed_at DATETIME NULL, + consumed_at DATETIME NULL, is_active BOOLEAN NOT NULL DEFAULT 1, - effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus', + effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount', extra_data TEXT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, @@ -752,8 +753,9 @@ async def create_discount_offers_table(): bonus_amount_kopeks INTEGER NOT NULL DEFAULT 0, expires_at TIMESTAMP NOT NULL, claimed_at TIMESTAMP NULL, + consumed_at TIMESTAMP NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, - effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus', + effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount', extra_data JSON NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP @@ -775,8 +777,9 @@ async def create_discount_offers_table(): bonus_amount_kopeks INTEGER NOT NULL DEFAULT 0, expires_at DATETIME NOT NULL, claimed_at DATETIME NULL, + consumed_at DATETIME NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, - effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus', + effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount', extra_data JSON NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, @@ -804,26 +807,19 @@ async def ensure_discount_offer_columns(): try: effect_exists = await check_column_exists('discount_offers', 'effect_type') extra_exists = await check_column_exists('discount_offers', 'extra_data') - - if effect_exists and extra_exists: - return True + consumed_exists = await check_column_exists('discount_offers', 'consumed_at') async with engine.begin() as conn: db_type = await get_database_type() if not effect_exists: + default_sql = "ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount'" if db_type == 'sqlite': - await conn.execute(text( - "ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus'" - )) + await conn.execute(text(default_sql)) elif db_type == 'postgresql': - await conn.execute(text( - "ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus'" - )) + await conn.execute(text(default_sql)) elif db_type == 'mysql': - await conn.execute(text( - "ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus'" - )) + await conn.execute(text(default_sql)) else: raise ValueError(f"Unsupported database type: {db_type}") @@ -843,7 +839,27 @@ async def ensure_discount_offer_columns(): else: raise ValueError(f"Unsupported database type: {db_type}") - logger.info("✅ Колонки effect_type и extra_data для discount_offers проверены") + if not consumed_exists: + if db_type == 'sqlite': + await conn.execute(text( + "ALTER TABLE discount_offers ADD COLUMN consumed_at DATETIME NULL" + )) + elif db_type == 'postgresql': + await conn.execute(text( + "ALTER TABLE discount_offers ADD COLUMN consumed_at TIMESTAMP NULL" + )) + elif db_type == 'mysql': + await conn.execute(text( + "ALTER TABLE discount_offers ADD COLUMN consumed_at DATETIME NULL" + )) + else: + raise ValueError(f"Unsupported database type: {db_type}") + + await conn.execute(text( + "UPDATE discount_offers SET effect_type = 'percent_discount' WHERE effect_type = 'balance_bonus'" + )) + + logger.info("✅ Колонки effect_type, extra_data и consumed_at для discount_offers проверены") return True except Exception as e: @@ -2651,6 +2667,7 @@ async def check_migration_status(): "discount_offers_table": False, "discount_offers_effect_column": False, "discount_offers_extra_column": False, + "discount_offers_consumed_column": False, "promo_offer_templates_table": False, "subscription_temporary_access_table": False, } @@ -2667,6 +2684,7 @@ async def check_migration_status(): status["discount_offers_table"] = await check_table_exists('discount_offers') status["discount_offers_effect_column"] = await check_column_exists('discount_offers', 'effect_type') status["discount_offers_extra_column"] = await check_column_exists('discount_offers', 'extra_data') + status["discount_offers_consumed_column"] = await check_column_exists('discount_offers', 'consumed_at') status["promo_offer_templates_table"] = await check_table_exists('promo_offer_templates') status["subscription_temporary_access_table"] = await check_table_exists('subscription_temporary_access') @@ -2717,6 +2735,7 @@ async def check_migration_status(): "users_auto_promo_group_assigned_column": "Флаг автоназначения промогруппы у пользователей", "users_auto_promo_group_threshold_column": "Порог последней авто-промогруппы у пользователей", "subscription_crypto_link_column": "Колонка subscription_crypto_link в subscriptions", + "discount_offers_consumed_column": "Колонка consumed_at у discount_offers", } for check_key, check_status in status.items(): diff --git a/app/handlers/admin/promo_offers.py b/app/handlers/admin/promo_offers.py index 249b3969..db409b1e 100644 --- a/app/handlers/admin/promo_offers.py +++ b/app/handlers/admin/promo_offers.py @@ -45,7 +45,7 @@ OFFER_TYPE_CONFIG = { "allowed_segments": [ ("paid_active", "🟢 Активные платные"), ], - "effect_type": "balance_bonus", + "effect_type": "percent_discount", }, "purchase_discount": { "icon": "🎯", @@ -55,7 +55,7 @@ OFFER_TYPE_CONFIG = { ("paid_expired", "🔴 Истёкшие платные"), ("trial_expired", "🥶 Истёкшие триалы"), ], - "effect_type": "balance_bonus", + "effect_type": "percent_discount", }, } diff --git a/app/handlers/subscription.py b/app/handlers/subscription.py index e8181f78..8d236c6a 100644 --- a/app/handlers/subscription.py +++ b/app/handlers/subscription.py @@ -9,14 +9,19 @@ from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings, PERIOD_PRICES, get_traffic_prices -from app.database.crud.discount_offer import get_offer_by_id, mark_offer_claimed +from app.database.crud.discount_offer import ( + get_active_percent_discount_offer, + get_offer_by_id, + mark_offer_claimed, + mark_offer_consumed, +) from app.database.crud.subscription import ( create_trial_subscription, create_paid_subscription, add_subscription_traffic, add_subscription_devices, update_subscription_autopay ) from app.database.crud.transaction import create_transaction -from app.database.crud.user import subtract_user_balance, add_user_balance +from app.database.crud.user import subtract_user_balance from app.database.models import ( User, TransactionType, SubscriptionStatus, Subscription @@ -140,6 +145,7 @@ def _apply_discount_to_monthly_component( async def _prepare_subscription_summary( + db: AsyncSession, db_user: User, data: Dict[str, Any], texts, @@ -246,7 +252,31 @@ async def _prepare_subscription_summary( if not is_valid: raise ValueError("Subscription price calculation validation failed") + offer_discount_percent = 0 + offer_discount_total = 0 + total_before_offer = total_price + applied_offer_id: Optional[int] = None + applied_offer_expires_at: Optional[str] = None + + active_offer = await get_active_percent_discount_offer(db, db_user.id) + if active_offer: + discounted_total, discount_value = apply_percentage_discount( + total_price, + active_offer.discount_percent, + ) + if discount_value > 0: + offer_discount_percent = active_offer.discount_percent + offer_discount_total = discount_value + total_price = discounted_total + applied_offer_id = active_offer.id + applied_offer_expires_at = active_offer.expires_at.isoformat() + summary_data['total_price'] = total_price + summary_data['total_price_before_offer'] = total_before_offer + summary_data['applied_discount_percent'] = offer_discount_percent + summary_data['applied_discount_total'] = offer_discount_total + summary_data['applied_discount_offer_id'] = applied_offer_id + summary_data['applied_discount_offer_expires_at'] = applied_offer_expires_at summary_data['server_prices_for_period'] = selected_server_prices summary_data['months_in_period'] = months_in_period summary_data['base_price'] = base_price @@ -330,8 +360,29 @@ async def _prepare_subscription_summary( ) details_lines.append(devices_line) + if offer_discount_total > 0: + original_total_display = texts.format_price(total_before_offer) + discount_line = ( + f"- Индивидуальная скидка: {original_total_display} " + f"{texts.format_price(total_price)}" + f" (скидка {offer_discount_percent}%:" + f" -{texts.format_price(offer_discount_total)})" + ) + details_lines.append(discount_line) + details_text = "\n".join(details_lines) + total_price_display: str + if offer_discount_total > 0: + total_price_display = ( + f"{texts.format_price(total_before_offer)} " + f"{texts.format_price(total_price)}" + f" (скидка {offer_discount_percent}%:" + f" -{texts.format_price(offer_discount_total)})" + ) + else: + total_price_display = texts.format_price(total_price) + summary_text = ( "📋 Сводка заказа\n\n" f"📅 Период: {period_display}\n" @@ -340,7 +391,7 @@ async def _prepare_subscription_summary( f"📱 Устройства: {devices_selected}\n\n" "💰 Детализация стоимости:\n" f"{details_text}\n\n" - f"💎 Общая стоимость: {texts.format_price(total_price)}\n\n" + f"💎 Общая стоимость: {total_price_display}\n\n" "Подтверждаете покупку?" ) @@ -3373,7 +3424,7 @@ async def devices_continue( texts = get_texts(db_user.language) try: - summary_text, prepared_data = await _prepare_subscription_summary(db_user, data, texts) + summary_text, prepared_data = await _prepare_subscription_summary(db, db_user, data, texts) except ValueError: logger.error(f"Ошибка в расчете цены подписки для пользователя {db_user.telegram_id}") await callback.answer("Ошибка расчета цены. Обратитесь в поддержку.", show_alert=True) @@ -3410,6 +3461,49 @@ async def confirm_purchase( else None ) + applied_discount_offer_id = data.get('applied_discount_offer_id') + applied_discount_percent = max(0, data.get('applied_discount_percent') or 0) + applied_discount_total = max(0, data.get('applied_discount_total') or 0) + total_price_before_offer = data.get('total_price_before_offer', data.get('total_price', 0)) + applied_offer_record = None + + if applied_discount_offer_id: + offer_record = await get_offer_by_id(db, applied_discount_offer_id) + now = datetime.utcnow() + if ( + not offer_record + or offer_record.effect_type not in {"percent_discount", "balance_bonus"} + or offer_record.consumed_at is not None + or offer_record.expires_at <= now + ): + try: + summary_text, prepared_data = await _prepare_subscription_summary(db, db_user, data, texts) + except ValueError: + logger.error( + "Не удалось пересчитать заказ подписки после истечения скидки пользователя %s", + db_user.telegram_id, + ) + await callback.answer( + texts.get("DISCOUNT_CLAIM_EXPIRED", "⚠️ Время действия предложения истекло"), + 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 callback.answer( + texts.get("DISCOUNT_CLAIM_EXPIRED", "⚠️ Время действия предложения истекло"), + show_alert=True, + ) + return + + applied_offer_record = offer_record + countries = await _get_available_countries(db_user.promo_group_id) months_in_period = data.get( @@ -3563,7 +3657,7 @@ async def confirm_purchase( base_price, discounted_monthly_additions, months_in_period, - final_price, + total_price_before_offer, ) if not is_valid: @@ -3612,6 +3706,12 @@ async def confirm_purchase( f" -{devices_discount_total / 100}₽)" ) logger.info(message) + if applied_discount_total > 0 and total_price_before_offer > final_price: + logger.info( + " Индивидуальная скидка: -%s₽ (скидка %s%%)", + applied_discount_total / 100, + applied_discount_percent, + ) logger.info(f" ИТОГО: {final_price / 100}₽") if db_user.balance_kopeks < final_price: @@ -3932,6 +4032,16 @@ async def confirm_purchase( ) purchase_completed = True + if applied_offer_record: + try: + await mark_offer_consumed(db, applied_offer_record) + except Exception as consume_error: + logger.warning( + "Не удалось отметить использование скидки %s для пользователя %s: %s", + applied_offer_record.id, + db_user.telegram_id, + consume_error, + ) logger.info( f"Пользователь {db_user.telegram_id} купил подписку на {data['period_days']} дней за {final_price / 100}₽") @@ -3953,6 +4063,7 @@ async def resume_subscription_checkout( callback: types.CallbackQuery, state: FSMContext, db_user: User, + db: AsyncSession, ): texts = get_texts(db_user.language) @@ -3963,7 +4074,7 @@ async def resume_subscription_checkout( return try: - summary_text, prepared_data = await _prepare_subscription_summary(db_user, draft, texts) + summary_text, prepared_data = await _prepare_subscription_summary(db, db_user, draft, texts) except ValueError as exc: logger.error( f"Ошибка восстановления заказа подписки для пользователя {db_user.telegram_id}: {exc}" @@ -5094,32 +5205,27 @@ async def claim_discount_offer( await callback.message.answer(success_message) return - bonus_amount = offer.bonus_amount_kopeks or 0 - if bonus_amount > 0: - success = await add_user_balance( - db, - db_user, - bonus_amount, - texts.get("DISCOUNT_BONUS_DESCRIPTION", "Скидка за продление подписки"), - ) - if not success: - await callback.answer( - texts.get("DISCOUNT_CLAIM_ERROR", "❌ Не удалось начислить скидку. Попробуйте позже."), - show_alert=True, - ) - return + discount_percent = max(0, offer.discount_percent or 0) + await mark_offer_claimed(db, offer, deactivate=False) - await mark_offer_claimed(db, offer) + expires_text = "" + if offer.expires_at: + expires_text = offer.expires_at.strftime("%d.%m.%Y %H:%M") success_message = texts.get( "DISCOUNT_CLAIM_SUCCESS", - "🎉 Скидка {percent}% активирована! На баланс начислено {amount}.", + ( + "🎉 Скидка {percent}% активирована!\n" + "Она автоматически применится к следующей оплате подписки и суммируется с вашей промогруппой.\n" + "Скидка действует до {expires_at}." + ), ).format( - percent=offer.discount_percent, - amount=settings.format_price(bonus_amount), + percent=discount_percent, + expires_at=expires_text, ) - await callback.answer("✅ Скидка активирована!", show_alert=True) + popup_text = texts.get("DISCOUNT_CLAIM_POPUP", "✅ Скидка активирована!") + await callback.answer(popup_text, show_alert=True) await callback.message.answer(success_message) diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json index 84750376..411d59f4 100644 --- a/app/localization/locales/en.json +++ b/app/localization/locales/en.json @@ -549,10 +549,11 @@ "DELETE_MESSAGE": "🗑 Delete", "DISCOUNT_BONUS_DESCRIPTION": "Renewal discount bonus", "DISCOUNT_CLAIM_ALREADY": "ℹ️ This discount has already been activated.", - "DISCOUNT_CLAIM_ERROR": "❌ Failed to credit the discount. Please try again later.", + "DISCOUNT_CLAIM_ERROR": "❌ Couldn't activate the discount. Please try again later.", "DISCOUNT_CLAIM_EXPIRED": "⚠️ The offer has expired.", "DISCOUNT_CLAIM_NOT_FOUND": "❌ Offer not found.", - "DISCOUNT_CLAIM_SUCCESS": "🎉 Discount of {percent}% activated! {amount} credited to your balance.", + "DISCOUNT_CLAIM_SUCCESS": "🎉 {percent}% discount activated! It will automatically apply to your next payment and stacks with your promo group. Valid until {expires_at}.", + "DISCOUNT_CLAIM_POPUP": "✅ Discount activated!", "ENTER_BLOCK_MINUTES": "Enter the number of minutes to block the user (e.g., 15):", "LANGUAGE_SELECTION_DISABLED": "⚙️ Language selection is temporarily unavailable. Using the default language.", "MARK_AS_ANSWERED": "✅ Mark as answered", @@ -592,8 +593,8 @@ "REPORT_CLOSE_ERROR": "❌ Failed to close the report.", "SENDING_ATTACHMENTS": "📎 Sending attachments...", "SUBSCRIPTION_EXPIRED_1D": "⛔ Your subscription expired\n\nAccess was disabled on {end_date}. Renew to return to the service.\n\n💎 Renewal price: {price}", - "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 {percent}% discount on renewal\n\nTap “Get discount” and we'll add {bonus} to your balance. The offer is valid until {expires_at}.", - "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Personal {percent}% discount\n\nIt's been {trigger_days} days without a subscription. Come back — tap “Get discount” and {bonus} will be credited. Offer valid until {expires_at}.", + "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 {percent}% discount on renewal\n\nTap “Get discount” — we'll apply {percent}% off at checkout. Save up to {bonus}. The offer is valid until {expires_at} and stacks with your promo group.", + "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Personal {percent}% discount\n\nIt's been {trigger_days} days without a subscription — come back and we'll automatically apply {percent}% off. Save up to {bonus}. The offer is valid until {expires_at} and stacks with your promo group.", "SUBSCRIPTION_EXTEND": "💎 Extend subscription", "SUBSCRIPTION_HAPP_CRYPTOLINK_BLOCK": "
{crypto_link}
", "SUBSCRIPTION_HAPP_LINK_PROMPT": "🔒 Subscription link is ready. Tap the \"Connect\" button below to open it in Happ.", diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json index 4ca51d14..8c848544 100644 --- a/app/localization/locales/ru.json +++ b/app/localization/locales/ru.json @@ -549,10 +549,11 @@ "DELETE_MESSAGE": "🗑 Удалить", "DISCOUNT_BONUS_DESCRIPTION": "Скидка за продление подписки", "DISCOUNT_CLAIM_ALREADY": "ℹ️ Скидка уже была активирована ранее.", - "DISCOUNT_CLAIM_ERROR": "❌ Не удалось начислить скидку. Попробуйте позже.", + "DISCOUNT_CLAIM_ERROR": "❌ Не удалось активировать скидку. Попробуйте позже.", "DISCOUNT_CLAIM_EXPIRED": "⚠️ Время действия предложения истекло.", "DISCOUNT_CLAIM_NOT_FOUND": "❌ Предложение не найдено.", - "DISCOUNT_CLAIM_SUCCESS": "🎉 Скидка {percent}% активирована! На баланс начислено {amount}.", + "DISCOUNT_CLAIM_SUCCESS": "🎉 Скидка {percent}% активирована! Она автоматически применится к следующей оплате и суммируется с вашей промогруппой. Действует до {expires_at}.", + "DISCOUNT_CLAIM_POPUP": "✅ Скидка активирована!", "ENTER_BLOCK_MINUTES": "Введите количество минут для блокировки пользователя (например, 15):", "LANGUAGE_SELECTION_DISABLED": "⚙️ Выбор языка временно недоступен. Используем язык по умолчанию.", "MARK_AS_ANSWERED": "✅ Отметить как отвеченный", @@ -592,8 +593,8 @@ "REPORT_CLOSE_ERROR": "❌ Не удалось закрыть отчет.", "SENDING_ATTACHMENTS": "📎 Отправляю вложения...", "SUBSCRIPTION_EXPIRED_1D": "⛔ Подписка закончилась\n\nДоступ был отключён {end_date}. Продлите подписку, чтобы вернуть полный доступ.\n\n💎 Стоимость продления: {price}", - "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 Скидка {percent}% на продление\n\nНажмите «Получить скидку», и мы начислим {bonus} на ваш баланс. Предложение действительно до {expires_at}.", - "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Индивидуальная скидка {percent}%\n\nПрошло {trigger_days} дней без подписки. Вернитесь — нажмите «Получить скидку», и {bonus} поступит на баланс. Предложение действительно до {expires_at}.", + "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 Скидка {percent}% на продление\n\nНажмите «Получить скидку» — мы применим {percent}% скидку при оплате. Экономия до {bonus}. Скидка действует до {expires_at} и суммируется с вашей промогруппой.", + "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Индивидуальная скидка {percent}%\n\nПрошло {trigger_days} дней без подписки — возвращайтесь, и скидка {percent}% автоматически применится при оплате. Экономия до {bonus}. Скидка действует до {expires_at} и суммируется с вашей промогруппой.", "SUBSCRIPTION_EXTEND": "💎 Продлить подписку", "SUBSCRIPTION_HAPP_CRYPTOLINK_BLOCK": "
{crypto_link}
", "SUBSCRIPTION_HAPP_LINK_PROMPT": "🔒 Ссылка на подписку создана. Нажмите кнопку \"Подключиться\" ниже, чтобы открыть её в Happ.", diff --git a/app/services/monitoring_service.py b/app/services/monitoring_service.py index 584e03fc..c150dcb6 100644 --- a/app/services/monitoring_service.py +++ b/app/services/monitoring_service.py @@ -1202,8 +1202,8 @@ class MonitoringService: "SUBSCRIPTION_EXPIRED_SECOND_WAVE", ( "🔥 Скидка {percent}% на продление\n\n" - "Нажмите «Получить скидку», и мы начислим {bonus} на баланс. " - "Предложение действует до {expires_at}." + "Нажмите «Получить скидку» — мы применим {percent}% скидку при оплате." + " Экономия до {bonus}. Скидка действует до {expires_at} и суммируется с вашей промогруппой." ), ) else: @@ -1211,8 +1211,9 @@ class MonitoringService: "SUBSCRIPTION_EXPIRED_THIRD_WAVE", ( "🎁 Индивидуальная скидка {percent}%\n\n" - "Прошло {trigger_days} дней без подписки — возвращайтесь, и мы добавим {bonus} на баланс. " - "Скидка действует до {expires_at}." + "Прошло {trigger_days} дней без подписки — возвращайтесь, и скидка {percent}% автоматически" + " применится при оплате. Экономия до {bonus}." + " Скидка действует до {expires_at} и суммируется с вашей промогруппой." ), ) diff --git a/locales/en.json b/locales/en.json index f024ad95..59c93b6e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -527,13 +527,14 @@ "TRIAL_INACTIVE_1H": "⏳ An hour has passed and we haven't seen any traffic yet\n\nOpen the connection guide and follow the steps. We're always ready to help!", "TRIAL_INACTIVE_24H": "⏳ A full day passed without activity\n\nWe still don't see traffic from your test subscription. Use the guide or message support and we'll help you connect!", "SUBSCRIPTION_EXPIRED_1D": "⛔ Your subscription expired\n\nAccess was disabled on {end_date}. Renew to return to the service.\n\n💎 Renewal price: {price}", - "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 {percent}% discount on renewal\n\nTap “Get discount” and we'll add {bonus} to your balance. The offer is valid until {expires_at}.", - "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Personal {percent}% discount\n\nIt's been {trigger_days} days without a subscription. Come back — tap “Get discount” and {bonus} will be credited. Offer valid until {expires_at}.", - "DISCOUNT_CLAIM_SUCCESS": "🎉 Discount of {percent}% activated! {amount} credited to your balance.", + "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 {percent}% discount on renewal\n\nTap “Get discount” — we'll apply {percent}% off at checkout. Save up to {bonus}. The offer is valid until {expires_at} and stacks with your promo group.", + "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Personal {percent}% discount\n\nIt's been {trigger_days} days without a subscription — come back and we'll automatically apply {percent}% off. Save up to {bonus}. The offer is valid until {expires_at} and stacks with your promo group.", + "DISCOUNT_CLAIM_SUCCESS": "🎉 {percent}% discount activated! It will automatically apply to your next payment and stacks with your promo group. Valid until {expires_at}.", "DISCOUNT_CLAIM_ALREADY": "ℹ️ This discount has already been activated.", "DISCOUNT_CLAIM_EXPIRED": "⚠️ The offer has expired.", "DISCOUNT_CLAIM_NOT_FOUND": "❌ Offer not found.", - "DISCOUNT_CLAIM_ERROR": "❌ Failed to credit the discount. Please try again later.", + "DISCOUNT_CLAIM_ERROR": "❌ Couldn't activate the discount. Please try again later.", + "DISCOUNT_CLAIM_POPUP": "✅ Discount activated!", "TEST_ACCESS_NO_SUBSCRIPTION": "❌ You need an active subscription to use this offer.", "TEST_ACCESS_NO_SQUADS": "❌ Unable to determine servers for the test access. Please contact support.", "TEST_ACCESS_UNKNOWN_ERROR": "❌ Failed to activate the offer. Please try again later.", diff --git a/locales/ru.json b/locales/ru.json index f4d63055..7891f522 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -527,13 +527,14 @@ "TRIAL_INACTIVE_1H": "⏳ Прошёл час, а подключение не выполнено\n\nЕсли возникли сложности — откройте инструкцию и следуйте шагам. Мы всегда готовы помочь!", "TRIAL_INACTIVE_24H": "⏳ Прошли сутки с начала теста\n\nМы не видим трафика по вашей подписке. Загляните в инструкцию или напишите в поддержку — поможем подключиться!", "SUBSCRIPTION_EXPIRED_1D": "⛔ Подписка закончилась\n\nДоступ был отключён {end_date}. Продлите подписку, чтобы вернуть полный доступ.\n\n💎 Стоимость продления: {price}", - "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 Скидка {percent}% на продление\n\nНажмите «Получить скидку», и мы начислим {bonus} на ваш баланс. Предложение действительно до {expires_at}.", - "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Индивидуальная скидка {percent}%\n\nПрошло {trigger_days} дней без подписки. Вернитесь — нажмите «Получить скидку», и {bonus} поступит на баланс. Предложение действительно до {expires_at}.", - "DISCOUNT_CLAIM_SUCCESS": "🎉 Скидка {percent}% активирована! На баланс начислено {amount}.", + "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 Скидка {percent}% на продление\n\nНажмите «Получить скидку» — мы применим {percent}% скидку при оплате. Экономия до {bonus}. Скидка действует до {expires_at} и суммируется с вашей промогруппой.", + "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Индивидуальная скидка {percent}%\n\nПрошло {trigger_days} дней без подписки — возвращайтесь, и скидка {percent}% автоматически применится при оплате. Экономия до {bonus}. Скидка действует до {expires_at} и суммируется с вашей промогруппой.", + "DISCOUNT_CLAIM_SUCCESS": "🎉 Скидка {percent}% активирована! Она автоматически применится к следующей оплате и суммируется с вашей промогруппой. Действует до {expires_at}.", "DISCOUNT_CLAIM_ALREADY": "ℹ️ Скидка уже была активирована ранее.", "DISCOUNT_CLAIM_EXPIRED": "⚠️ Время действия предложения истекло.", "DISCOUNT_CLAIM_NOT_FOUND": "❌ Предложение не найдено.", - "DISCOUNT_CLAIM_ERROR": "❌ Не удалось начислить скидку. Попробуйте позже.", + "DISCOUNT_CLAIM_ERROR": "❌ Не удалось активировать скидку. Попробуйте позже.", + "DISCOUNT_CLAIM_POPUP": "✅ Скидка активирована!", "TEST_ACCESS_NO_SUBSCRIPTION": "❌ Для активации предложения необходима действующая подписка.", "TEST_ACCESS_NO_SQUADS": "❌ Не удалось определить список серверов для теста. Обратитесь к администратору.", "TEST_ACCESS_UNKNOWN_ERROR": "❌ Не удалось активировать предложение. Попробуйте позже.",