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": "❌ Не удалось активировать предложение. Попробуйте позже.",