diff --git a/app/database/crud/discount_offer.py b/app/database/crud/discount_offer.py
index a7ef5f08..0ae6a068 100644
--- a/app/database/crud/discount_offer.py
+++ b/app/database/crud/discount_offer.py
@@ -14,9 +14,9 @@ async def upsert_discount_offer(
subscription_id: Optional[int],
notification_type: str,
discount_percent: int,
- bonus_amount_kopeks: int = 0,
+ bonus_amount_kopeks: int,
valid_hours: int,
- effect_type: str = "percent_discount",
+ effect_type: str = "balance_bonus",
extra_data: Optional[dict] = None,
) -> DiscountOffer:
"""Create or refresh a discount offer for a user."""
@@ -67,6 +67,14 @@ 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
+ await db.commit()
+ await db.refresh(offer)
+ return offer
+
+
async def deactivate_expired_offers(db: AsyncSession) -> int:
now = datetime.utcnow()
result = await db.execute(
@@ -86,44 +94,3 @@ async def deactivate_expired_offers(db: AsyncSession) -> int:
await db.commit()
return count
-
-
-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.is_active == True, # noqa: E712
- DiscountOffer.effect_type.in_(["percent_discount", "balance_bonus"]),
- DiscountOffer.claimed_at.isnot(None),
- DiscountOffer.expires_at > now,
- DiscountOffer.discount_percent > 0,
- )
- .order_by(DiscountOffer.expires_at.asc())
- )
- return result.scalars().first()
-
-
-async def mark_offer_claimed(
- db: AsyncSession,
- offer: DiscountOffer,
- *,
- deactivate: bool = True,
-) -> DiscountOffer:
- offer.claimed_at = datetime.utcnow()
- if deactivate:
- offer.is_active = False
- await db.commit()
- await db.refresh(offer)
- return offer
-
-
-async def consume_discount_offer(db: AsyncSession, offer: DiscountOffer) -> DiscountOffer:
- offer.is_active = False
- await db.commit()
- await db.refresh(offer)
- return offer
diff --git a/app/database/crud/promo_offer_template.py b/app/database/crud/promo_offer_template.py
index 714c31cc..cb99770b 100644
--- a/app/database/crud/promo_offer_template.py
+++ b/app/database/crud/promo_offer_template.py
@@ -31,13 +31,13 @@ DEFAULT_TEMPLATES: tuple[dict, ...] = (
"name": "Скидка на продление",
"message_text": (
"💎 Экономия {discount_percent}% при продлении\n\n"
- "Активируйте предложение — скидка автоматически уменьшит стоимость следующей оплаты.\n"
+ "Мы начислим {bonus_amount} на баланс после активации, чтобы продление обошлось дешевле.\n"
"Срок действия предложения — {valid_hours} ч."
),
"button_text": "🎁 Получить скидку",
"valid_hours": 24,
"discount_percent": 20,
- "bonus_amount_kopeks": 0,
+ "bonus_amount_kopeks": settings.PRICE_30_DAYS * 20 // 100,
"test_duration_hours": None,
"test_squad_uuids": [],
},
@@ -46,13 +46,13 @@ DEFAULT_TEMPLATES: tuple[dict, ...] = (
"name": "Скидка на покупку",
"message_text": (
"🎯 Вернитесь со скидкой {discount_percent}%\n\n"
- "Скидка суммируется с вашей промо-группой и уменьшит стоимость новой подписки.\n"
+ "Начислим {bonus_amount} после активации — используйте бонус при оплате новой подписки.\n"
"Предложение действует {valid_hours} ч."
),
"button_text": "🎁 Забрать скидку",
"valid_hours": 48,
"discount_percent": 25,
- "bonus_amount_kopeks": 0,
+ "bonus_amount_kopeks": settings.PRICE_30_DAYS * 25 // 100,
"test_duration_hours": None,
"test_squad_uuids": [],
},
diff --git a/app/database/models.py b/app/database/models.py
index eeb08dd2..6de613e4 100644
--- a/app/database/models.py
+++ b/app/database/models.py
@@ -812,7 +812,7 @@ class DiscountOffer(Base):
expires_at = Column(DateTime, nullable=False)
claimed_at = Column(DateTime, nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
- effect_type = Column(String(50), nullable=False, default="percent_discount")
+ effect_type = Column(String(50), nullable=False, default="balance_bonus")
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 0527035f..4d5dd201 100644
--- a/app/database/universal_migration.py
+++ b/app/database/universal_migration.py
@@ -728,7 +728,7 @@ async def create_discount_offers_table():
expires_at DATETIME NOT NULL,
claimed_at DATETIME NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
- effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount',
+ effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus',
extra_data TEXT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -753,7 +753,7 @@ async def create_discount_offers_table():
expires_at TIMESTAMP NOT NULL,
claimed_at TIMESTAMP NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
- effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount',
+ effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus',
extra_data JSON NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
@@ -776,7 +776,7 @@ async def create_discount_offers_table():
expires_at DATETIME NOT NULL,
claimed_at DATETIME NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
- effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount',
+ effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus',
extra_data JSON NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
@@ -814,15 +814,15 @@ async def ensure_discount_offer_columns():
if not effect_exists:
if db_type == 'sqlite':
await conn.execute(text(
- "ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount'"
+ "ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus'"
))
elif db_type == 'postgresql':
await conn.execute(text(
- "ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount'"
+ "ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus'"
))
elif db_type == 'mysql':
await conn.execute(text(
- "ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount'"
+ "ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus'"
))
else:
raise ValueError(f"Unsupported database type: {db_type}")
@@ -851,37 +851,6 @@ async def ensure_discount_offer_columns():
return False
-async def update_discount_offer_effect_defaults() -> bool:
- try:
- effect_exists = await check_column_exists('discount_offers', 'effect_type')
- if not effect_exists:
- return True
-
- async with engine.begin() as conn:
- db_type = await get_database_type()
-
- if db_type == 'postgresql':
- await conn.execute(text(
- "ALTER TABLE discount_offers ALTER COLUMN effect_type SET DEFAULT 'percent_discount'"
- ))
- elif db_type == 'mysql':
- await conn.execute(text(
- "ALTER TABLE discount_offers ALTER COLUMN effect_type SET DEFAULT 'percent_discount'"
- ))
- # SQLite requires table rebuild to change defaults; skip altering default but update rows
-
- await conn.execute(text(
- "UPDATE discount_offers SET effect_type = 'percent_discount' WHERE effect_type = 'balance_bonus'"
- ))
-
- logger.info("✅ Значение по умолчанию effect_type для discount_offers обновлено")
- return True
-
- except Exception as e:
- logger.error(f"Ошибка обновления значения по умолчанию effect_type: {e}")
- return False
-
-
async def create_promo_offer_templates_table():
table_exists = await check_table_exists('promo_offer_templates')
if table_exists:
@@ -2464,12 +2433,6 @@ async def run_universal_migration():
else:
logger.warning("⚠️ Не удалось обновить колонки discount_offers")
- discount_defaults_ready = await update_discount_offer_effect_defaults()
- if discount_defaults_ready:
- logger.info("✅ Значение по умолчанию скидочного эффекта обновлено")
- else:
- logger.warning("⚠️ Не удалось обновить значение по умолчанию скидочного эффекта")
-
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ PROMO_OFFER_TEMPLATES ===")
promo_templates_created = await create_promo_offer_templates_table()
if promo_templates_created:
@@ -2688,7 +2651,6 @@ async def check_migration_status():
"discount_offers_table": False,
"discount_offers_effect_column": False,
"discount_offers_extra_column": False,
- "discount_offers_effect_default": False,
"promo_offer_templates_table": False,
"subscription_temporary_access_table": False,
}
@@ -2708,14 +2670,6 @@ async def check_migration_status():
status["promo_offer_templates_table"] = await check_table_exists('promo_offer_templates')
status["subscription_temporary_access_table"] = await check_table_exists('subscription_temporary_access')
- if status["discount_offers_effect_column"]:
- async with engine.begin() as conn:
- result = await conn.execute(text(
- "SELECT COUNT(*) FROM discount_offers WHERE effect_type = 'balance_bonus'"
- ))
- remaining_legacy = result.scalar() or 0
- status["discount_offers_effect_default"] = remaining_legacy == 0
-
status["welcome_texts_is_enabled_column"] = await check_column_exists('welcome_texts', 'is_enabled')
status["users_promo_group_column"] = await check_column_exists('users', 'promo_group_id')
status["promo_groups_period_discounts_column"] = await check_column_exists('promo_groups', 'period_discounts')
diff --git a/app/handlers/admin/promo_offers.py b/app/handlers/admin/promo_offers.py
index 081c2ba8..249b3969 100644
--- a/app/handlers/admin/promo_offers.py
+++ b/app/handlers/admin/promo_offers.py
@@ -10,6 +10,7 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
from sqlalchemy.ext.asyncio import AsyncSession
+from app.config import settings
from app.database.crud.discount_offer import upsert_discount_offer
from app.database.crud.promo_offer_template import (
ensure_default_templates,
@@ -44,7 +45,7 @@ OFFER_TYPE_CONFIG = {
"allowed_segments": [
("paid_active", "🟢 Активные платные"),
],
- "effect_type": "percent_discount",
+ "effect_type": "balance_bonus",
},
"purchase_discount": {
"icon": "🎯",
@@ -54,13 +55,18 @@ OFFER_TYPE_CONFIG = {
("paid_expired", "🔴 Истёкшие платные"),
("trial_expired", "🥶 Истёкшие триалы"),
],
- "effect_type": "percent_discount",
+ "effect_type": "balance_bonus",
},
}
+def _format_bonus(template: PromoOfferTemplate) -> str:
+ return settings.format_price(template.bonus_amount_kopeks or 0)
+
+
def _render_template_text(template: PromoOfferTemplate, language: str) -> str:
replacements = {
"discount_percent": template.discount_percent,
+ "bonus_amount": _format_bonus(template),
"valid_hours": template.valid_hours,
"test_duration_hours": template.test_duration_hours or 0,
}
@@ -103,6 +109,9 @@ def _build_offer_detail_keyboard(template: PromoOfferTemplate, language: str) ->
if template.offer_type != "test_access":
rows[-1].append(InlineKeyboardButton(text="📉 %", callback_data=f"promo_offer_edit_discount_{template.id}"))
+ rows.append([
+ InlineKeyboardButton(text="💰 Бонус", callback_data=f"promo_offer_edit_bonus_{template.id}"),
+ ])
else:
rows.append([
InlineKeyboardButton(text="⏳ Длительность", callback_data=f"promo_offer_edit_duration_{template.id}"),
@@ -147,12 +156,7 @@ def _describe_offer(template: PromoOfferTemplate, language: str) -> str:
if template.offer_type != "test_access":
lines.append(texts.t("ADMIN_PROMO_OFFER_DISCOUNT", "Скидка: {percent}%").format(percent=template.discount_percent))
- lines.append(
- texts.t(
- "ADMIN_PROMO_OFFER_STACKING",
- "Скидка суммируется с промогруппой и применяется автоматически.",
- )
- )
+ lines.append(texts.t("ADMIN_PROMO_OFFER_BONUS", "Бонус: {amount}").format(amount=_format_bonus(template)))
else:
duration = template.test_duration_hours or 0
lines.append(texts.t("ADMIN_PROMO_OFFER_TEST_DURATION", "Доступ: {hours} ч").format(hours=duration))
@@ -262,6 +266,15 @@ async def prompt_edit_discount(callback: CallbackQuery, db_user: User, db: Async
await _prompt_edit(callback, state, template_id, prompt, AdminStates.editing_promo_offer_discount)
+@admin_required
+@error_handler
+async def prompt_edit_bonus(callback: CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext):
+ template_id = int(callback.data.split("_")[-1])
+ texts = get_texts(db_user.language)
+ prompt = texts.t("ADMIN_PROMO_OFFER_PROMPT_BONUS", "Введите размер бонуса в копейках:")
+ await _prompt_edit(callback, state, template_id, prompt, AdminStates.editing_promo_offer_bonus)
+
+
@admin_required
@error_handler
async def prompt_edit_duration(callback: CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext):
@@ -315,6 +328,9 @@ async def _handle_edit_field(
elif field == "discount_percent":
percent = max(0, min(100, int(value)))
await update_promo_offer_template(db, template, discount_percent=percent)
+ elif field == "bonus_amount_kopeks":
+ bonus = max(0, int(value))
+ await update_promo_offer_template(db, template, bonus_amount_kopeks=bonus)
elif field == "test_duration_hours":
hours = max(1, int(value))
await update_promo_offer_template(db, template, test_duration_hours=hours)
@@ -408,7 +424,7 @@ async def send_offer_to_segment(callback: CallbackQuery, db_user: User, db: Asyn
sent = 0
failed = 0
- effect_type = config.get("effect_type", "percent_discount")
+ effect_type = config.get("effect_type", "balance_bonus")
for user in users:
try:
@@ -418,6 +434,7 @@ async def send_offer_to_segment(callback: CallbackQuery, db_user: User, db: Asyn
subscription_id=user.subscription.id if user.subscription else None,
notification_type=f"promo_template_{template.id}",
discount_percent=template.discount_percent,
+ bonus_amount_kopeks=template.bonus_amount_kopeks,
valid_hours=template.valid_hours,
effect_type=effect_type,
extra_data={
@@ -478,6 +495,10 @@ async def process_edit_discount_percent(message: Message, state: FSMContext, db:
await _handle_edit_field(message, state, db, db_user, "discount_percent")
+async def process_edit_bonus_amount(message: Message, state: FSMContext, db: AsyncSession, db_user: User):
+ await _handle_edit_field(message, state, db, db_user, "bonus_amount_kopeks")
+
+
async def process_edit_test_duration(message: Message, state: FSMContext, db: AsyncSession, db_user: User):
await _handle_edit_field(message, state, db, db_user, "test_duration_hours")
@@ -492,6 +513,7 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(prompt_edit_button, F.data.startswith("promo_offer_edit_button_"))
dp.callback_query.register(prompt_edit_valid, F.data.startswith("promo_offer_edit_valid_"))
dp.callback_query.register(prompt_edit_discount, F.data.startswith("promo_offer_edit_discount_"))
+ dp.callback_query.register(prompt_edit_bonus, F.data.startswith("promo_offer_edit_bonus_"))
dp.callback_query.register(prompt_edit_duration, F.data.startswith("promo_offer_edit_duration_"))
dp.callback_query.register(prompt_edit_squads, F.data.startswith("promo_offer_edit_squads_"))
dp.callback_query.register(show_send_segments, F.data.startswith("promo_offer_send_menu_"))
@@ -502,5 +524,6 @@ def register_handlers(dp: Dispatcher):
dp.message.register(process_edit_button_text, AdminStates.editing_promo_offer_button)
dp.message.register(process_edit_valid_hours, AdminStates.editing_promo_offer_valid_hours)
dp.message.register(process_edit_discount_percent, AdminStates.editing_promo_offer_discount)
+ dp.message.register(process_edit_bonus_amount, AdminStates.editing_promo_offer_bonus)
dp.message.register(process_edit_test_duration, AdminStates.editing_promo_offer_test_duration)
dp.message.register(process_edit_test_squads, AdminStates.editing_promo_offer_squads)
diff --git a/app/handlers/subscription.py b/app/handlers/subscription.py
index e9ca6b12..e8181f78 100644
--- a/app/handlers/subscription.py
+++ b/app/handlers/subscription.py
@@ -9,12 +9,7 @@ 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 (
- consume_discount_offer,
- get_active_percent_discount_offer,
- get_offer_by_id,
- mark_offer_claimed,
-)
+from app.database.crud.discount_offer import get_offer_by_id, mark_offer_claimed
from app.database.crud.subscription import (
create_trial_subscription,
create_paid_subscription, add_subscription_traffic, add_subscription_devices,
@@ -144,34 +139,10 @@ def _apply_discount_to_monthly_component(
}
-def _is_percent_discount_offer_active(offer: Optional["DiscountOffer"]) -> bool:
- if not offer:
- return False
-
- effect_type = (offer.effect_type or "").lower()
- if effect_type not in {"percent_discount", "balance_bonus"}:
- return False
-
- if not getattr(offer, "is_active", False):
- return False
-
- if getattr(offer, "claimed_at", None) is None:
- return False
-
- expires_at = getattr(offer, "expires_at", None)
- if expires_at and expires_at <= datetime.utcnow():
- return False
-
- percent = getattr(offer, "discount_percent", 0) or 0
- return percent > 0
-
-
async def _prepare_subscription_summary(
db_user: User,
data: Dict[str, Any],
texts,
- db: AsyncSession,
- discount_offer: Optional["DiscountOffer"] = None,
) -> Tuple[str, Dict[str, Any]]:
summary_data = dict(data)
countries = await _get_available_countries(db_user.promo_group_id)
@@ -301,29 +272,6 @@ async def _prepare_subscription_summary(
summary_data['devices_discounted_price_per_month'] = devices_component["discounted_per_month"]
summary_data['total_devices_price'] = total_devices_price
summary_data['discounted_monthly_additions'] = discounted_monthly_additions
- summary_data['total_price_before_discount'] = total_price
- summary_data['percent_discount_offer_id'] = None
- summary_data['percent_discount_percent'] = 0
- summary_data['percent_discount_amount'] = 0
-
- effective_discount_offer = discount_offer
- if effective_discount_offer is None:
- try:
- effective_discount_offer = await get_active_percent_discount_offer(db, db_user.id)
- except Exception:
- effective_discount_offer = None
-
- if _is_percent_discount_offer_active(effective_discount_offer):
- discount_percent = max(0, min(100, effective_discount_offer.discount_percent or 0))
- discounted_total, discount_value = apply_percentage_discount(
- total_price,
- discount_percent,
- )
- summary_data['total_price'] = discounted_total
- summary_data['percent_discount_offer_id'] = effective_discount_offer.id
- summary_data['percent_discount_percent'] = discount_percent
- summary_data['percent_discount_amount'] = discount_value
- total_price = discounted_total
if settings.is_traffic_fixed():
if final_traffic_gb == 0:
@@ -384,20 +332,6 @@ async def _prepare_subscription_summary(
details_text = "\n".join(details_lines)
- final_price = summary_data['total_price']
- discount_amount_total = summary_data.get('percent_discount_amount', 0) or 0
- discount_percent_total = summary_data.get('percent_discount_percent', 0) or 0
-
- discount_line = ""
- if discount_amount_total > 0 and discount_percent_total > 0:
- discount_line = texts.t(
- "SUBSCRIPTION_PERSONAL_DISCOUNT",
- "🎁 Персональная скидка: {percent}% (-{amount})",
- ).format(
- percent=discount_percent_total,
- amount=texts.format_price(discount_amount_total),
- )
-
summary_text = (
"📋 Сводка заказа\n\n"
f"📅 Период: {period_display}\n"
@@ -406,14 +340,8 @@ async def _prepare_subscription_summary(
f"📱 Устройства: {devices_selected}\n\n"
"💰 Детализация стоимости:\n"
f"{details_text}\n\n"
- )
-
- if discount_line:
- summary_text += f"{discount_line}\n"
-
- summary_text += (
- f"💎 Общая стоимость: {texts.format_price(final_price)}\n\n"
- + texts.t("SUBSCRIPTION_PURCHASE_CONFIRM", "Подтверждаете покупку?")
+ f"💎 Общая стоимость: {texts.format_price(total_price)}\n\n"
+ "Подтверждаете покупку?"
)
return summary_text, summary_data
@@ -2445,13 +2373,7 @@ async def handle_extend_subscription(
subscription_service = SubscriptionService()
available_periods = settings.get_available_renewal_periods()
- renewal_prices: Dict[int, int] = {}
- renewal_discount_amounts: Dict[int, int] = {}
-
- active_discount_offer = await get_active_percent_discount_offer(db, db_user.id)
- applied_discount_percent = 0
- if _is_percent_discount_offer_active(active_discount_offer):
- applied_discount_percent = max(0, min(100, active_discount_offer.discount_percent or 0))
+ renewal_prices = {}
for days in available_periods:
try:
@@ -2497,17 +2419,7 @@ async def handle_extend_subscription(
total_traffic_price = (traffic_price_per_month - traffic_discount_per_month) * months_in_period
price = base_price + total_servers_price + total_devices_price + total_traffic_price
-
- if applied_discount_percent > 0:
- discounted_price, discount_value = apply_percentage_discount(
- price,
- applied_discount_percent,
- )
- renewal_prices[days] = discounted_price
- renewal_discount_amounts[days] = discount_value
- else:
- renewal_prices[days] = price
- renewal_discount_amounts[days] = 0
+ renewal_prices[days] = price
except Exception as e:
logger.error(f"Ошибка расчета цены для периода {days}: {e}")
@@ -2522,16 +2434,7 @@ async def handle_extend_subscription(
for days in available_periods:
if days in renewal_prices:
period_display = format_period_description(days, db_user.language)
- price_value = renewal_prices[days]
- line = f"📅 {period_display} - {texts.format_price(price_value)}"
- if applied_discount_percent > 0:
- discount_amount = renewal_discount_amounts.get(days, 0) or 0
- if discount_amount > 0:
- line += (
- f" (скидка {applied_discount_percent}%:"
- f" -{texts.format_price(discount_amount)})"
- )
- prices_text += f"{line}\n"
+ prices_text += f"📅 {period_display} - {texts.format_price(renewal_prices[days])}\n"
promo_discounts_text = _build_promo_group_discount_text(
db_user,
@@ -2555,15 +2458,6 @@ async def handle_extend_subscription(
message_text += "💡 Цена включает все ваши текущие серверы и настройки"
- if applied_discount_percent > 0:
- message_text += (
- "\n\n"
- + texts.t(
- "SUBSCRIPTION_PERSONAL_DISCOUNT_HINT",
- "🎁 Персональная скидка {percent}% будет применена автоматически при оплате.",
- ).format(percent=applied_discount_percent)
- )
-
await callback.message.edit_text(
message_text,
reply_markup=get_extend_subscription_keyboard_with_prices(db_user.language, renewal_prices),
@@ -2827,22 +2721,6 @@ async def confirm_extend_subscription(
price = base_price + total_servers_price + total_devices_price + total_traffic_price
- applied_discount_offer = None
- applied_discount_percent = 0
- applied_discount_amount = 0
-
- active_discount_offer = await get_active_percent_discount_offer(db, db_user.id)
- if _is_percent_discount_offer_active(active_discount_offer):
- applied_discount_percent = max(0, min(100, active_discount_offer.discount_percent or 0))
- discounted_price, discount_value = apply_percentage_discount(
- price,
- applied_discount_percent,
- )
- if discount_value > 0:
- price = discounted_price
- applied_discount_offer = active_discount_offer
- applied_discount_amount = discount_value
-
monthly_additions = (
discounted_servers_price_per_month
+ discounted_devices_price_per_month
@@ -3026,12 +2904,6 @@ async def confirm_extend_subscription(
f"💰 Списано: {texts.format_price(price)}"
)
- if applied_discount_amount > 0 and applied_discount_percent > 0:
- success_message += (
- f" (скидка {applied_discount_percent}%:"
- f" -{texts.format_price(applied_discount_amount)})"
- )
-
await callback.message.edit_text(
success_message,
reply_markup=get_back_keyboard(db_user.language)
@@ -3049,16 +2921,6 @@ async def confirm_extend_subscription(
reply_markup=get_back_keyboard(db_user.language)
)
- if applied_discount_offer:
- try:
- await consume_discount_offer(db, applied_discount_offer)
- except Exception as discount_error:
- logger.error(
- "Не удалось деактивировать персональную скидку пользователя %s: %s",
- db_user.telegram_id,
- discount_error,
- )
-
await callback.answer()
@@ -3511,7 +3373,7 @@ async def devices_continue(
texts = get_texts(db_user.language)
try:
- summary_text, prepared_data = await _prepare_subscription_summary(db_user, data, texts, db)
+ summary_text, prepared_data = await _prepare_subscription_summary(db_user, data, texts)
except ValueError:
logger.error(f"Ошибка в расчете цены подписки для пользователя {db_user.telegram_id}")
await callback.answer("Ошибка расчета цены. Обратитесь в поддержку.", show_alert=True)
@@ -3688,81 +3550,7 @@ async def confirm_purchase(
total_servers_price = data.get('total_servers_price', total_countries_price)
- final_price = data.get('total_price', 0)
- original_total_price = data.get('total_price_before_discount', final_price)
- stored_discount_offer_id = data.get('percent_discount_offer_id')
- applied_discount_offer = None
- applied_discount_percent = 0
- applied_discount_amount = 0
-
- if stored_discount_offer_id:
- candidate_offer = await get_offer_by_id(db, int(stored_discount_offer_id))
- if not _is_percent_discount_offer_active(candidate_offer):
- data['percent_discount_offer_id'] = None
- data['percent_discount_percent'] = 0
- data['percent_discount_amount'] = 0
- data['total_price'] = original_total_price
- await state.set_data(data)
- try:
- summary_text, prepared_data = await _prepare_subscription_summary(db_user, data, texts, db)
- except ValueError:
- await callback.answer(texts.ERROR, show_alert=True)
- return
- await state.set_data(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.t(
- "SUBSCRIPTION_PERSONAL_DISCOUNT_EXPIRED",
- "⚠️ Персональная скидка больше не доступна. Стоимость пересчитана.",
- ),
- show_alert=True,
- )
- return
- applied_discount_offer = candidate_offer
- else:
- candidate_offer = await get_active_percent_discount_offer(db, db_user.id)
- if _is_percent_discount_offer_active(candidate_offer):
- summary_text, prepared_data = await _prepare_subscription_summary(
- db_user,
- data,
- texts,
- db,
- discount_offer=candidate_offer,
- )
- await state.set_data(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.t(
- "SUBSCRIPTION_PERSONAL_DISCOUNT_APPLIED",
- "✅ Персональная скидка {percent}% применена. Проверьте обновлённую сумму.",
- ).format(percent=candidate_offer.discount_percent),
- show_alert=True,
- )
- return
- if applied_discount_offer:
- applied_discount_percent = max(0, min(100, applied_discount_offer.discount_percent or 0))
- final_price, applied_discount_amount = apply_percentage_discount(
- original_total_price,
- applied_discount_percent,
- )
-
- discount_note = ""
- if applied_discount_amount > 0 and applied_discount_percent > 0:
- discount_note = texts.t(
- "SUBSCRIPTION_PERSONAL_DISCOUNT_NOTE",
- "🎁 Персональная скидка {percent}%: -{amount}.",
- ).format(
- percent=applied_discount_percent,
- amount=texts.format_price(applied_discount_amount),
- )
+ final_price = data['total_price']
discounted_monthly_additions = data.get(
'discounted_monthly_additions',
@@ -4062,9 +3850,6 @@ async def confirm_purchase(
f"{texts.t('SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT', '📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве')}"
)
- if discount_note:
- success_text += f"\n\n{discount_note}"
-
connect_mode = settings.CONNECT_BUTTON_MODE
if connect_mode == "miniapp_subscription":
@@ -4138,27 +3923,14 @@ async def confirm_purchase(
parse_mode="HTML"
)
else:
- purchase_text = texts.SUBSCRIPTION_PURCHASED
- if discount_note:
- purchase_text = f"{purchase_text}\n\n{discount_note}"
await callback.message.edit_text(
texts.t(
"SUBSCRIPTION_LINK_GENERATING_NOTICE",
"{purchase_text}\n\nСсылка генерируется, перейдите в раздел 'Моя подписка' через несколько секунд.",
- ).format(purchase_text=purchase_text),
+ ).format(purchase_text=texts.SUBSCRIPTION_PURCHASED),
reply_markup=get_back_keyboard(db_user.language)
)
- if applied_discount_offer:
- try:
- await consume_discount_offer(db, applied_discount_offer)
- except Exception as discount_error:
- logger.error(
- "Не удалось деактивировать персональную скидку пользователя %s: %s",
- db_user.telegram_id,
- discount_error,
- )
-
purchase_completed = True
logger.info(
f"Пользователь {db_user.telegram_id} купил подписку на {data['period_days']} дней за {final_price / 100}₽")
@@ -4181,7 +3953,6 @@ async def resume_subscription_checkout(
callback: types.CallbackQuery,
state: FSMContext,
db_user: User,
- db: AsyncSession,
):
texts = get_texts(db_user.language)
@@ -4192,7 +3963,7 @@ async def resume_subscription_checkout(
return
try:
- summary_text, prepared_data = await _prepare_subscription_summary(db_user, draft, texts, db)
+ summary_text, prepared_data = await _prepare_subscription_summary(db_user, draft, texts)
except ValueError as exc:
logger.error(
f"Ошибка восстановления заказа подписки для пользователя {db_user.telegram_id}: {exc}"
@@ -5323,25 +5094,32 @@ async def claim_discount_offer(
await callback.message.answer(success_message)
return
- if effect_type not in {"percent_discount", "balance_bonus"}:
- await callback.answer(
- texts.get("DISCOUNT_CLAIM_NOT_SUPPORTED", "⚠️ Этот тип предложения пока не поддерживается."),
- show_alert=True,
+ 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", "Скидка за продление подписки"),
)
- return
+ if not success:
+ await callback.answer(
+ texts.get("DISCOUNT_CLAIM_ERROR", "❌ Не удалось начислить скидку. Попробуйте позже."),
+ show_alert=True,
+ )
+ return
- await mark_offer_claimed(db, offer, deactivate=False)
+ await mark_offer_claimed(db, offer)
- expires_text = offer.expires_at.strftime("%d.%m.%Y %H:%M") if offer.expires_at else ""
success_message = texts.get(
"DISCOUNT_CLAIM_SUCCESS",
- "🎉 Скидка {percent}% активирована! Она автоматически применится при следующей оплате и действует до {expires_at}.",
+ "🎉 Скидка {percent}% активирована! На баланс начислено {amount}.",
).format(
percent=offer.discount_percent,
- expires_at=expires_text,
+ amount=settings.format_price(bonus_amount),
)
- await callback.answer(texts.get("DISCOUNT_CLAIM_POPUP", "✅ Скидка активирована!"), show_alert=True)
+ await callback.answer("✅ Скидка активирована!", show_alert=True)
await callback.message.answer(success_message)
diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json
index 9ebbb238..84750376 100644
--- a/app/localization/locales/en.json
+++ b/app/localization/locales/en.json
@@ -547,18 +547,12 @@
"CONTACT_SUPPORT_BUTTON": "💬 Contact support",
"CREATE_TICKET_BUTTON": "🎫 Create ticket",
"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_EXPIRED": "⚠️ The offer has expired.",
"DISCOUNT_CLAIM_NOT_FOUND": "❌ Offer not found.",
- "DISCOUNT_CLAIM_POPUP": "✅ Discount activated!",
- "DISCOUNT_CLAIM_NOT_SUPPORTED": "⚠️ This offer type is not supported yet.",
- "DISCOUNT_CLAIM_SUCCESS": "🎉 Discount of {percent}% activated! It will be applied automatically to your next payment and is valid until {expires_at}.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT": "🎁 Personal discount: {percent}% (-{amount})",
- "SUBSCRIPTION_PURCHASE_CONFIRM": "Do you confirm the purchase?",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_HINT": "🎁 A personal discount of {percent}% will be applied automatically at checkout.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_NOTE": "🎁 Personal discount {percent}%: -{amount}.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_APPLIED": "✅ Personal discount of {percent}% applied. Please review the updated total.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_EXPIRED": "⚠️ The personal discount is no longer available. The price has been recalculated.",
+ "DISCOUNT_CLAIM_SUCCESS": "🎉 Discount of {percent}% activated! {amount} credited to your balance.",
"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",
diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json
index 9576e106..4ca51d14 100644
--- a/app/localization/locales/ru.json
+++ b/app/localization/locales/ru.json
@@ -547,18 +547,12 @@
"CONTACT_SUPPORT_BUTTON": "💬 Связаться с поддержкой",
"CREATE_TICKET_BUTTON": "🎫 Создать тикет",
"DELETE_MESSAGE": "🗑 Удалить",
+ "DISCOUNT_BONUS_DESCRIPTION": "Скидка за продление подписки",
"DISCOUNT_CLAIM_ALREADY": "ℹ️ Скидка уже была активирована ранее.",
+ "DISCOUNT_CLAIM_ERROR": "❌ Не удалось начислить скидку. Попробуйте позже.",
"DISCOUNT_CLAIM_EXPIRED": "⚠️ Время действия предложения истекло.",
"DISCOUNT_CLAIM_NOT_FOUND": "❌ Предложение не найдено.",
- "DISCOUNT_CLAIM_POPUP": "✅ Скидка активирована!",
- "DISCOUNT_CLAIM_NOT_SUPPORTED": "⚠️ Этот тип предложения пока не поддерживается.",
- "DISCOUNT_CLAIM_SUCCESS": "🎉 Скидка {percent}% активирована! Она суммируется с вашей промо-группой и действует до {expires_at}.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT": "🎁 Персональная скидка: {percent}% (-{amount})",
- "SUBSCRIPTION_PURCHASE_CONFIRM": "Подтверждаете покупку?",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_HINT": "🎁 Персональная скидка {percent}% будет применена автоматически при оплате.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_NOTE": "🎁 Персональная скидка {percent}%: -{amount}.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_APPLIED": "✅ Персональная скидка {percent}% применена. Проверьте обновлённую сумму.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_EXPIRED": "⚠️ Персональная скидка больше не доступна. Стоимость пересчитана.",
+ "DISCOUNT_CLAIM_SUCCESS": "🎉 Скидка {percent}% активирована! На баланс начислено {amount}.",
"ENTER_BLOCK_MINUTES": "Введите количество минут для блокировки пользователя (например, 15):",
"LANGUAGE_SELECTION_DISABLED": "⚙️ Выбор языка временно недоступен. Используем язык по умолчанию.",
"MARK_AS_ANSWERED": "✅ Отметить как отвеченный",
diff --git a/app/services/monitoring_service.py b/app/services/monitoring_service.py
index e6cb2399..584e03fc 100644
--- a/app/services/monitoring_service.py
+++ b/app/services/monitoring_service.py
@@ -681,12 +681,14 @@ class MonitoringService:
if not await notification_sent(db, user.id, subscription.id, "expired_discount_wave2"):
percent = NotificationSettingsService.get_second_wave_discount_percent()
valid_hours = NotificationSettingsService.get_second_wave_valid_hours()
+ bonus_amount = settings.PRICE_30_DAYS * percent // 100
offer = await upsert_discount_offer(
db,
user_id=user.id,
subscription_id=subscription.id,
notification_type="expired_discount_wave2",
discount_percent=percent,
+ bonus_amount_kopeks=bonus_amount,
valid_hours=valid_hours,
)
success = await self._send_expired_discount_notification(
@@ -696,6 +698,7 @@ class MonitoringService:
offer.expires_at,
offer.id,
"second",
+ bonus_amount,
)
if success:
await record_notification(db, user.id, subscription.id, "expired_discount_wave2")
@@ -708,12 +711,14 @@ class MonitoringService:
if not await notification_sent(db, user.id, subscription.id, "expired_discount_wave3"):
percent = NotificationSettingsService.get_third_wave_discount_percent()
valid_hours = NotificationSettingsService.get_third_wave_valid_hours()
+ bonus_amount = settings.PRICE_30_DAYS * percent // 100
offer = await upsert_discount_offer(
db,
user_id=user.id,
subscription_id=subscription.id,
notification_type="expired_discount_wave3",
discount_percent=percent,
+ bonus_amount_kopeks=bonus_amount,
valid_hours=valid_hours,
)
success = await self._send_expired_discount_notification(
@@ -723,6 +728,7 @@ class MonitoringService:
offer.expires_at,
offer.id,
"third",
+ bonus_amount,
trigger_days=trigger_days,
)
if success:
@@ -1185,6 +1191,7 @@ class MonitoringService:
expires_at: datetime,
offer_id: int,
wave: str,
+ bonus_amount: int,
trigger_days: int = None,
) -> bool:
try:
@@ -1195,8 +1202,8 @@ class MonitoringService:
"SUBSCRIPTION_EXPIRED_SECOND_WAVE",
(
"🔥 Скидка {percent}% на продление\n\n"
- "Нажмите «Получить скидку», и мы автоматически уменьшим стоимость продления. "
- "Скидка суммируется с вашей промо-группой и действует до {expires_at}."
+ "Нажмите «Получить скидку», и мы начислим {bonus} на баланс. "
+ "Предложение действует до {expires_at}."
),
)
else:
@@ -1204,13 +1211,14 @@ class MonitoringService:
"SUBSCRIPTION_EXPIRED_THIRD_WAVE",
(
"🎁 Индивидуальная скидка {percent}%\n\n"
- "Прошло {trigger_days} дней без подписки — возвращайтесь, и скидка уменьшит стоимость новой подписки. "
- "Скидка суммируется с вашей промо-группой и действует до {expires_at}."
+ "Прошло {trigger_days} дней без подписки — возвращайтесь, и мы добавим {bonus} на баланс. "
+ "Скидка действует до {expires_at}."
),
)
message = template.format(
percent=percent,
+ bonus=settings.format_price(bonus_amount),
expires_at=expires_at.strftime("%d.%m.%Y %H:%M"),
trigger_days=trigger_days or "",
)
diff --git a/app/states.py b/app/states.py
index 4bee07ca..767d20f6 100644
--- a/app/states.py
+++ b/app/states.py
@@ -110,6 +110,7 @@ class AdminStates(StatesGroup):
editing_promo_offer_button = State()
editing_promo_offer_valid_hours = State()
editing_promo_offer_discount = State()
+ editing_promo_offer_bonus = State()
editing_promo_offer_test_duration = State()
editing_promo_offer_squads = State()
diff --git a/locales/en.json b/locales/en.json
index d8b389e2..f024ad95 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -527,25 +527,19 @@
"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 the renewal price will drop automatically. The discount stacks with your promo group and is valid until {expires_at}.",
- "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Personal {percent}% discount\n\nIt's been {trigger_days} days without a subscription — come back and this discount will lower the price of your new plan. It stacks with your promo group and is valid until {expires_at}.",
- "DISCOUNT_CLAIM_SUCCESS": "🎉 Discount of {percent}% activated! It will be applied automatically to your next payment and is valid until {expires_at}.",
+ "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.",
"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_POPUP": "✅ Discount activated!",
- "DISCOUNT_CLAIM_NOT_SUPPORTED": "⚠️ This offer type is not supported yet.",
+ "DISCOUNT_CLAIM_ERROR": "❌ Failed to credit the discount. Please try again later.",
"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.",
"TEST_ACCESS_ACTIVATED_MESSAGE": "🎉 Test servers are connected! Access is active until {expires_at}.",
"TEST_ACCESS_ACTIVATED_POPUP": "✅ Access granted!",
- "SUBSCRIPTION_PERSONAL_DISCOUNT": "🎁 Personal discount: {percent}% (-{amount})",
- "SUBSCRIPTION_PURCHASE_CONFIRM": "Do you confirm the purchase?",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_HINT": "🎁 A personal discount of {percent}% will be applied automatically at checkout.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_NOTE": "🎁 Personal discount {percent}%: -{amount}.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_APPLIED": "✅ Personal discount of {percent}% applied. Please review the updated total.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_EXPIRED": "⚠️ The personal discount is no longer available. The price has been recalculated.",
+ "DISCOUNT_BONUS_DESCRIPTION": "Renewal discount bonus",
"NOTIFICATION_VALUE_INVALID": "❌ Invalid value, please enter a number.",
"NOTIFICATION_VALUE_UPDATED": "✅ Settings updated.",
"NOTIFY_PROMPT_SECOND_PERCENT": "Enter a new discount percentage for the 2-3 day reminder (0-100):",
@@ -576,7 +570,7 @@
"ADMIN_PROMO_OFFER_TYPE": "Type: {label}",
"ADMIN_PROMO_OFFER_VALID": "Validity: {hours} h",
"ADMIN_PROMO_OFFER_DISCOUNT": "Discount: {percent}%",
- "ADMIN_PROMO_OFFER_STACKING": "Discount stacks with the promo group and is applied automatically.",
+ "ADMIN_PROMO_OFFER_BONUS": "Bonus: {amount}",
"ADMIN_PROMO_OFFER_TEST_DURATION": "Access: {hours} h",
"ADMIN_PROMO_OFFER_TEST_SQUADS": "Squads: {squads}",
"ADMIN_PROMO_OFFER_TEST_SQUADS_EMPTY": "Squads: not specified",
@@ -586,6 +580,7 @@
"ADMIN_PROMO_OFFER_PROMPT_BUTTON": "Enter the button label:",
"ADMIN_PROMO_OFFER_PROMPT_VALID": "Enter validity (hours):",
"ADMIN_PROMO_OFFER_PROMPT_DISCOUNT": "Enter discount percentage:",
+ "ADMIN_PROMO_OFFER_PROMPT_BONUS": "Enter bonus amount in kopeks:",
"ADMIN_PROMO_OFFER_PROMPT_DURATION": "Enter test access duration (hours):",
"ADMIN_PROMO_OFFER_PROMPT_SQUADS": "List squad UUIDs separated by commas or spaces. Send 'clear' to reset:",
"ADMIN_PROMO_OFFER_SENDING": "Starting broadcast...",
diff --git a/locales/ru.json b/locales/ru.json
index 36e36fe2..f4d63055 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -527,25 +527,19 @@
"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Нажмите «Получить скидку», и цена продления уменьшится автоматически. Скидка суммируется с вашей промо-группой и действует до {expires_at}.",
- "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Индивидуальная скидка {percent}%\n\nПрошло {trigger_days} дней без подписки — возвращайтесь, и эта скидка уменьшит стоимость новой подписки. Скидка суммируется с вашей промо-группой и действует до {expires_at}.",
- "DISCOUNT_CLAIM_SUCCESS": "🎉 Скидка {percent}% активирована! Она суммируется с вашей промо-группой и действует до {expires_at}.",
+ "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}.",
"DISCOUNT_CLAIM_ALREADY": "ℹ️ Скидка уже была активирована ранее.",
"DISCOUNT_CLAIM_EXPIRED": "⚠️ Время действия предложения истекло.",
"DISCOUNT_CLAIM_NOT_FOUND": "❌ Предложение не найдено.",
- "DISCOUNT_CLAIM_POPUP": "✅ Скидка активирована!",
- "DISCOUNT_CLAIM_NOT_SUPPORTED": "⚠️ Этот тип предложения пока не поддерживается.",
+ "DISCOUNT_CLAIM_ERROR": "❌ Не удалось начислить скидку. Попробуйте позже.",
"TEST_ACCESS_NO_SUBSCRIPTION": "❌ Для активации предложения необходима действующая подписка.",
"TEST_ACCESS_NO_SQUADS": "❌ Не удалось определить список серверов для теста. Обратитесь к администратору.",
"TEST_ACCESS_UNKNOWN_ERROR": "❌ Не удалось активировать предложение. Попробуйте позже.",
"TEST_ACCESS_ACTIVATED_MESSAGE": "🎉 Тестовые сервера подключены! Доступ активен до {expires_at}.",
"TEST_ACCESS_ACTIVATED_POPUP": "✅ Доступ выдан!",
- "SUBSCRIPTION_PERSONAL_DISCOUNT": "🎁 Персональная скидка: {percent}% (-{amount})",
- "SUBSCRIPTION_PURCHASE_CONFIRM": "Подтверждаете покупку?",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_HINT": "🎁 Персональная скидка {percent}% будет применена автоматически при оплате.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_NOTE": "🎁 Персональная скидка {percent}%: -{amount}.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_APPLIED": "✅ Персональная скидка {percent}% применена. Проверьте обновлённую сумму.",
- "SUBSCRIPTION_PERSONAL_DISCOUNT_EXPIRED": "⚠️ Персональная скидка больше не доступна. Стоимость пересчитана.",
+ "DISCOUNT_BONUS_DESCRIPTION": "Скидка за продление подписки",
"NOTIFICATION_VALUE_INVALID": "❌ Некорректное значение, укажите число.",
"NOTIFICATION_VALUE_UPDATED": "✅ Настройки обновлены.",
"NOTIFY_PROMPT_SECOND_PERCENT": "Введите новый процент скидки для уведомления через 2-3 дня (0-100):",
@@ -576,7 +570,7 @@
"ADMIN_PROMO_OFFER_TYPE": "Тип: {label}",
"ADMIN_PROMO_OFFER_VALID": "Срок действия: {hours} ч",
"ADMIN_PROMO_OFFER_DISCOUNT": "Скидка: {percent}%",
- "ADMIN_PROMO_OFFER_STACKING": "Скидка суммируется с промогруппой и применяется автоматически.",
+ "ADMIN_PROMO_OFFER_BONUS": "Бонус: {amount}",
"ADMIN_PROMO_OFFER_TEST_DURATION": "Доступ: {hours} ч",
"ADMIN_PROMO_OFFER_TEST_SQUADS": "Сквады: {squads}",
"ADMIN_PROMO_OFFER_TEST_SQUADS_EMPTY": "Сквады: не указаны",
@@ -586,6 +580,7 @@
"ADMIN_PROMO_OFFER_PROMPT_BUTTON": "Введите новый текст кнопки:",
"ADMIN_PROMO_OFFER_PROMPT_VALID": "Укажите срок действия (в часах):",
"ADMIN_PROMO_OFFER_PROMPT_DISCOUNT": "Введите размер скидки в процентах:",
+ "ADMIN_PROMO_OFFER_PROMPT_BONUS": "Введите размер бонуса в копейках:",
"ADMIN_PROMO_OFFER_PROMPT_DURATION": "Введите длительность тестового доступа (в часах):",
"ADMIN_PROMO_OFFER_PROMPT_SQUADS": "Перечислите UUID сквадов через запятую или пробел. Для очистки отправьте 'clear':",
"ADMIN_PROMO_OFFER_SENDING": "Начинаем рассылку...",