mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-05-05 04:16:17 +00:00
Clear expired promo discounts before checkout
This commit is contained in:
@@ -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."""
|
||||
|
||||
@@ -31,13 +31,13 @@ DEFAULT_TEMPLATES: tuple[dict, ...] = (
|
||||
"name": "Скидка на продление",
|
||||
"message_text": (
|
||||
"💎 <b>Экономия {discount_percent}% при продлении</b>\n\n"
|
||||
"Мы начислим {bonus_amount} на баланс после активации, чтобы продление обошлось дешевле.\n"
|
||||
"Срок действия предложения — {valid_hours} ч."
|
||||
"Активируйте предложение, и дополнительная скидка {discount_percent}% автоматически применится при оплате.\n"
|
||||
"Скидка суммируется с вашей промогруппой и действует {valid_hours} ч."
|
||||
),
|
||||
"button_text": "🎁 Получить скидку",
|
||||
"valid_hours": 24,
|
||||
"discount_percent": 20,
|
||||
"bonus_amount_kopeks": settings.PRICE_30_DAYS * 20 // 100,
|
||||
"bonus_amount_kopeks": 0,
|
||||
"test_duration_hours": None,
|
||||
"test_squad_uuids": [],
|
||||
},
|
||||
@@ -46,13 +46,13 @@ DEFAULT_TEMPLATES: tuple[dict, ...] = (
|
||||
"name": "Скидка на покупку",
|
||||
"message_text": (
|
||||
"🎯 <b>Вернитесь со скидкой {discount_percent}%</b>\n\n"
|
||||
"Начислим {bonus_amount} после активации — используйте бонус при оплате новой подписки.\n"
|
||||
"Скидка {discount_percent}% автоматически применится при покупке новой подписки и суммируется с промогруппой.\n"
|
||||
"Предложение действует {valid_hours} ч."
|
||||
),
|
||||
"button_text": "🎁 Забрать скидку",
|
||||
"valid_hours": 48,
|
||||
"discount_percent": 25,
|
||||
"bonus_amount_kopeks": settings.PRICE_30_DAYS * 25 // 100,
|
||||
"bonus_amount_kopeks": 0,
|
||||
"test_duration_hours": None,
|
||||
"test_squad_uuids": [],
|
||||
},
|
||||
|
||||
@@ -315,6 +315,24 @@ async def subtract_user_balance(
|
||||
return False
|
||||
|
||||
|
||||
async def clear_user_pending_discount(db: AsyncSession, user: User) -> None:
|
||||
try:
|
||||
user.pending_discount_percent = 0
|
||||
user.pending_discount_expires_at = None
|
||||
user.pending_discount_offer_id = None
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
logger.info(f"🔄 Сброшена активная скидка для пользователя {user.id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка сброса скидки пользователя {user.id}: {e}")
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception as rollback_error:
|
||||
logger.error(f"Ошибка отката транзакции при сбросе скидки пользователя {user.id}: {rollback_error}")
|
||||
|
||||
|
||||
async def get_users_list(
|
||||
db: AsyncSession,
|
||||
offset: int = 0,
|
||||
|
||||
@@ -396,7 +396,10 @@ class User(Base):
|
||||
has_made_first_topup: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
promo_group_id = Column(Integer, ForeignKey("promo_groups.id", ondelete="RESTRICT"), nullable=False, index=True)
|
||||
promo_group = relationship("PromoGroup", back_populates="users")
|
||||
|
||||
pending_discount_percent = Column(Integer, nullable=False, default=0)
|
||||
pending_discount_expires_at = Column(DateTime, nullable=True)
|
||||
pending_discount_offer_id = Column(Integer, ForeignKey("discount_offers.id", ondelete="SET NULL"), nullable=True)
|
||||
|
||||
@property
|
||||
def balance_rubles(self) -> float:
|
||||
return self.balance_kopeks / 100
|
||||
@@ -812,7 +815,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="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())
|
||||
|
||||
@@ -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 '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,
|
||||
@@ -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 '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
|
||||
@@ -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 '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,
|
||||
@@ -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 'balance_bonus'"
|
||||
"ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount'"
|
||||
))
|
||||
elif db_type == 'postgresql':
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus'"
|
||||
"ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount'"
|
||||
))
|
||||
elif db_type == 'mysql':
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'balance_bonus'"
|
||||
"ALTER TABLE discount_offers ADD COLUMN effect_type VARCHAR(50) NOT NULL DEFAULT 'percent_discount'"
|
||||
))
|
||||
else:
|
||||
raise ValueError(f"Unsupported database type: {db_type}")
|
||||
@@ -843,6 +843,13 @@ async def ensure_discount_offer_columns():
|
||||
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 для discount_offers проверены")
|
||||
return True
|
||||
|
||||
@@ -1911,6 +1918,64 @@ async def add_referral_system_columns():
|
||||
logger.error(f"Ошибка миграции реферальной системы: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def ensure_user_pending_discount_columns() -> bool:
|
||||
try:
|
||||
percent_exists = await check_column_exists('users', 'pending_discount_percent')
|
||||
expires_exists = await check_column_exists('users', 'pending_discount_expires_at')
|
||||
offer_exists = await check_column_exists('users', 'pending_discount_offer_id')
|
||||
|
||||
if percent_exists and expires_exists and offer_exists:
|
||||
return True
|
||||
|
||||
async with engine.begin() as conn:
|
||||
db_type = await get_database_type()
|
||||
|
||||
if not percent_exists:
|
||||
if db_type == 'sqlite':
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE users ADD COLUMN pending_discount_percent INTEGER NOT NULL DEFAULT 0"
|
||||
))
|
||||
elif db_type == 'postgresql':
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE users ADD COLUMN pending_discount_percent INTEGER NOT NULL DEFAULT 0"
|
||||
))
|
||||
elif db_type == 'mysql':
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE users ADD COLUMN pending_discount_percent INTEGER NOT NULL DEFAULT 0"
|
||||
))
|
||||
else:
|
||||
raise ValueError(f"Unsupported database type: {db_type}")
|
||||
|
||||
if not expires_exists:
|
||||
if db_type == 'sqlite':
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE users ADD COLUMN pending_discount_expires_at DATETIME NULL"
|
||||
))
|
||||
elif db_type == 'postgresql':
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE users ADD COLUMN pending_discount_expires_at TIMESTAMP NULL"
|
||||
))
|
||||
elif db_type == 'mysql':
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE users ADD COLUMN pending_discount_expires_at DATETIME NULL"
|
||||
))
|
||||
else:
|
||||
raise ValueError(f"Unsupported database type: {db_type}")
|
||||
|
||||
if not offer_exists:
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE users ADD COLUMN pending_discount_offer_id INTEGER NULL"
|
||||
))
|
||||
|
||||
logger.info("✅ Колонки pending_discount_* для users проверены")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления колонок скидок пользователя: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def create_subscription_conversions_table():
|
||||
table_exists = await check_table_exists('subscription_conversions')
|
||||
if table_exists:
|
||||
@@ -2372,6 +2437,10 @@ async def run_universal_migration():
|
||||
if not referral_migration_success:
|
||||
logger.warning("⚠️ Проблемы с миграцией реферальной системы")
|
||||
|
||||
pending_discount_columns_ready = await ensure_user_pending_discount_columns()
|
||||
if not pending_discount_columns_ready:
|
||||
logger.warning("⚠️ Не удалось обновить колонки скидок пользователя")
|
||||
|
||||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ SYSTEM_SETTINGS ===")
|
||||
system_settings_ready = await create_system_settings_table()
|
||||
if system_settings_ready:
|
||||
@@ -2653,6 +2722,9 @@ async def check_migration_status():
|
||||
"discount_offers_extra_column": False,
|
||||
"promo_offer_templates_table": False,
|
||||
"subscription_temporary_access_table": False,
|
||||
"users_pending_discount_percent_column": False,
|
||||
"users_pending_discount_expires_column": False,
|
||||
"users_pending_discount_offer_column": False,
|
||||
}
|
||||
|
||||
status["has_made_first_topup_column"] = await check_column_exists('users', 'has_made_first_topup')
|
||||
@@ -2678,6 +2750,9 @@ async def check_migration_status():
|
||||
status["users_auto_promo_group_assigned_column"] = await check_column_exists('users', 'auto_promo_group_assigned')
|
||||
status["users_auto_promo_group_threshold_column"] = await check_column_exists('users', 'auto_promo_group_threshold_kopeks')
|
||||
status["subscription_crypto_link_column"] = await check_column_exists('subscriptions', 'subscription_crypto_link')
|
||||
status["users_pending_discount_percent_column"] = await check_column_exists('users', 'pending_discount_percent')
|
||||
status["users_pending_discount_expires_column"] = await check_column_exists('users', 'pending_discount_expires_at')
|
||||
status["users_pending_discount_offer_column"] = await check_column_exists('users', 'pending_discount_offer_id')
|
||||
|
||||
media_fields_exist = (
|
||||
await check_column_exists('broadcast_history', 'has_media') and
|
||||
@@ -2717,6 +2792,9 @@ 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",
|
||||
"users_pending_discount_percent_column": "Колонка pending_discount_percent у пользователей",
|
||||
"users_pending_discount_expires_column": "Колонка pending_discount_expires_at у пользователей",
|
||||
"users_pending_discount_offer_column": "Колонка pending_discount_offer_id у пользователей",
|
||||
}
|
||||
|
||||
for check_key, check_status in status.items():
|
||||
|
||||
@@ -226,18 +226,16 @@ def _build_notification_preview_message(language: str, notification_type: str):
|
||||
elif notification_type == "expired_2d":
|
||||
percent = NotificationSettingsService.get_second_wave_discount_percent()
|
||||
valid_hours = NotificationSettingsService.get_second_wave_valid_hours()
|
||||
bonus_amount = settings.PRICE_30_DAYS * percent // 100
|
||||
template = texts.get(
|
||||
"SUBSCRIPTION_EXPIRED_SECOND_WAVE",
|
||||
(
|
||||
"🔥 <b>Скидка {percent}% на продление</b>\n\n"
|
||||
"Нажмите «Получить скидку», и мы начислим {bonus} на баланс. "
|
||||
"Предложение действует до {expires_at}."
|
||||
"Активируйте предложение — дополнительная скидка {percent}% автоматически применится при оплате. "
|
||||
"Скидка суммируется с вашей промогруппой и действует до {expires_at}."
|
||||
),
|
||||
)
|
||||
message = template.format(
|
||||
percent=percent,
|
||||
bonus=settings.format_price(bonus_amount),
|
||||
expires_at=(now + timedelta(hours=valid_hours)).strftime("%d.%m.%Y %H:%M"),
|
||||
trigger_days=3,
|
||||
)
|
||||
@@ -273,18 +271,16 @@ def _build_notification_preview_message(language: str, notification_type: str):
|
||||
percent = NotificationSettingsService.get_third_wave_discount_percent()
|
||||
valid_hours = NotificationSettingsService.get_third_wave_valid_hours()
|
||||
trigger_days = NotificationSettingsService.get_third_wave_trigger_days()
|
||||
bonus_amount = settings.PRICE_30_DAYS * percent // 100
|
||||
template = texts.get(
|
||||
"SUBSCRIPTION_EXPIRED_THIRD_WAVE",
|
||||
(
|
||||
"🎁 <b>Индивидуальная скидка {percent}%</b>\n\n"
|
||||
"Прошло {trigger_days} дней без подписки — возвращайтесь, и мы добавим {bonus} на баланс. "
|
||||
"Скидка действует до {expires_at}."
|
||||
"Прошло {trigger_days} дней без подписки — возвращайтесь, и скидка {percent}% автоматически применится при оплате. "
|
||||
"Скидка суммируется с вашей промогруппой и действует до {expires_at}."
|
||||
),
|
||||
)
|
||||
message = template.format(
|
||||
percent=percent,
|
||||
bonus=settings.format_price(bonus_amount),
|
||||
trigger_days=trigger_days,
|
||||
expires_at=(now + timedelta(hours=valid_hours)).strftime("%d.%m.%Y %H:%M"),
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -109,9 +109,6 @@ 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}"),
|
||||
@@ -156,7 +153,6 @@ 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_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))
|
||||
@@ -424,17 +420,22 @@ async def send_offer_to_segment(callback: CallbackQuery, db_user: User, db: Asyn
|
||||
|
||||
sent = 0
|
||||
failed = 0
|
||||
effect_type = config.get("effect_type", "balance_bonus")
|
||||
effect_type = config.get("effect_type", "percent_discount")
|
||||
|
||||
for user in users:
|
||||
try:
|
||||
bonus_amount = (
|
||||
template.bonus_amount_kopeks
|
||||
if effect_type not in {"percent_discount"}
|
||||
else 0
|
||||
)
|
||||
offer_record = await upsert_discount_offer(
|
||||
db,
|
||||
user_id=user.id,
|
||||
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,
|
||||
bonus_amount_kopeks=bonus_amount,
|
||||
valid_hours=template.valid_hours,
|
||||
effect_type=effect_type,
|
||||
extra_data={
|
||||
|
||||
@@ -16,7 +16,7 @@ from app.database.crud.subscription import (
|
||||
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, clear_user_pending_discount
|
||||
from app.database.models import (
|
||||
User, TransactionType, SubscriptionStatus,
|
||||
Subscription
|
||||
@@ -139,10 +139,36 @@ def _apply_discount_to_monthly_component(
|
||||
}
|
||||
|
||||
|
||||
async def _get_pending_offer_discount(
|
||||
user: User,
|
||||
db: Optional[AsyncSession] = None,
|
||||
) -> Tuple[int, Optional[datetime]]:
|
||||
percent = getattr(user, "pending_discount_percent", 0) or 0
|
||||
if percent <= 0:
|
||||
return 0, None
|
||||
|
||||
expires_at = getattr(user, "pending_discount_expires_at", None)
|
||||
if expires_at and expires_at < datetime.utcnow():
|
||||
if db is not None:
|
||||
try:
|
||||
await clear_user_pending_discount(db, user)
|
||||
percent = 0
|
||||
expires_at = getattr(user, "pending_discount_expires_at", None)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Не удалось сбросить просроченную скидку пользователя %s",
|
||||
getattr(user, "id", "unknown"),
|
||||
)
|
||||
return 0, expires_at
|
||||
|
||||
return percent, expires_at
|
||||
|
||||
|
||||
async def _prepare_subscription_summary(
|
||||
db_user: User,
|
||||
data: Dict[str, Any],
|
||||
texts,
|
||||
db: Optional[AsyncSession] = None,
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
summary_data = dict(data)
|
||||
countries = await _get_available_countries(db_user.promo_group_id)
|
||||
@@ -246,6 +272,22 @@ async def _prepare_subscription_summary(
|
||||
if not is_valid:
|
||||
raise ValueError("Subscription price calculation validation failed")
|
||||
|
||||
original_total_price = total_price
|
||||
offer_discount_percent, pending_discount_expires_at = await _get_pending_offer_discount(
|
||||
db_user,
|
||||
db,
|
||||
)
|
||||
offer_discount_total = 0
|
||||
|
||||
if offer_discount_percent > 0:
|
||||
total_price, offer_discount_total = apply_percentage_discount(
|
||||
original_total_price,
|
||||
offer_discount_percent,
|
||||
)
|
||||
|
||||
summary_data['total_price_before_offer'] = original_total_price
|
||||
summary_data['pending_discount_percent'] = offer_discount_percent
|
||||
summary_data['pending_discount_total'] = offer_discount_total
|
||||
summary_data['total_price'] = total_price
|
||||
summary_data['server_prices_for_period'] = selected_server_prices
|
||||
summary_data['months_in_period'] = months_in_period
|
||||
@@ -330,8 +372,28 @@ async def _prepare_subscription_summary(
|
||||
)
|
||||
details_lines.append(devices_line)
|
||||
|
||||
if offer_discount_total > 0:
|
||||
discount_line = texts.t(
|
||||
"SUBSCRIPTION_EXTRA_DISCOUNT_LINE",
|
||||
"- Доп. скидка по предложению: -{amount} ({percent}%)",
|
||||
).format(
|
||||
amount=texts.format_price(offer_discount_total),
|
||||
percent=offer_discount_percent,
|
||||
)
|
||||
details_lines.append(discount_line)
|
||||
|
||||
details_text = "\n".join(details_lines)
|
||||
|
||||
total_line = f"💎 <b>Общая стоимость:</b> {texts.format_price(total_price)}"
|
||||
if offer_discount_total > 0:
|
||||
total_line += texts.t(
|
||||
"SUBSCRIPTION_TOTAL_WITH_EXTRA_DISCOUNT",
|
||||
" (включая доп. скидку {percent}%: -{amount})",
|
||||
).format(
|
||||
percent=offer_discount_percent,
|
||||
amount=texts.format_price(offer_discount_total),
|
||||
)
|
||||
|
||||
summary_text = (
|
||||
"📋 <b>Сводка заказа</b>\n\n"
|
||||
f"📅 <b>Период:</b> {period_display}\n"
|
||||
@@ -340,10 +402,26 @@ async def _prepare_subscription_summary(
|
||||
f"📱 <b>Устройства:</b> {devices_selected}\n\n"
|
||||
"💰 <b>Детализация стоимости:</b>\n"
|
||||
f"{details_text}\n\n"
|
||||
f"💎 <b>Общая стоимость:</b> {texts.format_price(total_price)}\n\n"
|
||||
"Подтверждаете покупку?"
|
||||
f"{total_line}\n\n"
|
||||
)
|
||||
|
||||
if (
|
||||
offer_discount_percent > 0
|
||||
and pending_discount_expires_at
|
||||
and pending_discount_expires_at > datetime.utcnow()
|
||||
):
|
||||
summary_text += (
|
||||
texts.t(
|
||||
"SUBSCRIPTION_EXTRA_DISCOUNT_NOTE",
|
||||
"⏳ Доп. скидка действует до {expires_at}.",
|
||||
).format(
|
||||
expires_at=pending_discount_expires_at.strftime("%d.%m.%Y %H:%M"),
|
||||
)
|
||||
+ "\n\n"
|
||||
)
|
||||
|
||||
summary_text += "Подтверждаете покупку?"
|
||||
|
||||
return summary_text, summary_data
|
||||
|
||||
|
||||
@@ -3373,7 +3451,12 @@ 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_user,
|
||||
data,
|
||||
texts,
|
||||
db,
|
||||
)
|
||||
except ValueError:
|
||||
logger.error(f"Ошибка в расчете цены подписки для пользователя {db_user.telegram_id}")
|
||||
await callback.answer("Ошибка расчета цены. Обратитесь в поддержку.", show_alert=True)
|
||||
@@ -3550,6 +3633,9 @@ async def confirm_purchase(
|
||||
|
||||
total_servers_price = data.get('total_servers_price', total_countries_price)
|
||||
|
||||
original_total_price = data.get('total_price_before_offer', data['total_price'])
|
||||
offer_discount_percent = data.get('pending_discount_percent', 0)
|
||||
offer_discount_total = data.get('pending_discount_total', 0)
|
||||
final_price = data['total_price']
|
||||
|
||||
discounted_monthly_additions = data.get(
|
||||
@@ -3563,7 +3649,7 @@ async def confirm_purchase(
|
||||
base_price,
|
||||
discounted_monthly_additions,
|
||||
months_in_period,
|
||||
final_price,
|
||||
original_total_price,
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
@@ -3571,6 +3657,17 @@ async def confirm_purchase(
|
||||
await callback.answer("Ошибка расчета цены. Обратитесь в поддержку.", show_alert=True)
|
||||
return
|
||||
|
||||
if offer_discount_percent > 0:
|
||||
recalculated_total, expected_discount = apply_percentage_discount(
|
||||
original_total_price,
|
||||
offer_discount_percent,
|
||||
)
|
||||
if final_price != recalculated_total:
|
||||
final_price = recalculated_total
|
||||
offer_discount_total = expected_discount
|
||||
else:
|
||||
offer_discount_total = 0
|
||||
|
||||
logger.info(f"Расчет покупки подписки на {data['period_days']} дней ({months_in_period} мес):")
|
||||
base_log = f" Период: {base_price_original / 100}₽"
|
||||
if base_discount_total and base_discount_total > 0:
|
||||
@@ -3612,6 +3709,11 @@ async def confirm_purchase(
|
||||
f" -{devices_discount_total / 100}₽)"
|
||||
)
|
||||
logger.info(message)
|
||||
if offer_discount_total > 0:
|
||||
logger.info(
|
||||
f" Доп. скидка предложения: -{offer_discount_total / 100}₽"
|
||||
f" ({offer_discount_percent}%)"
|
||||
)
|
||||
logger.info(f" ИТОГО: {final_price / 100}₽")
|
||||
|
||||
if db_user.balance_kopeks < final_price:
|
||||
@@ -3943,6 +4045,12 @@ async def confirm_purchase(
|
||||
)
|
||||
|
||||
if purchase_completed:
|
||||
if (
|
||||
getattr(db_user, 'pending_discount_percent', 0) > 0
|
||||
or getattr(db_user, 'pending_discount_offer_id', None) is not None
|
||||
or getattr(db_user, 'pending_discount_expires_at', None) is not None
|
||||
):
|
||||
await clear_user_pending_discount(db, db_user)
|
||||
await clear_subscription_checkout_draft(db_user.id)
|
||||
|
||||
await state.clear()
|
||||
@@ -3953,6 +4061,7 @@ async def resume_subscription_checkout(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
@@ -3963,7 +4072,12 @@ 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_user,
|
||||
draft,
|
||||
texts,
|
||||
db,
|
||||
)
|
||||
except ValueError as exc:
|
||||
logger.error(
|
||||
f"Ошибка восстановления заказа подписки для пользователя {db_user.telegram_id}: {exc}"
|
||||
@@ -5053,7 +5167,7 @@ async def claim_discount_offer(
|
||||
)
|
||||
return
|
||||
|
||||
effect_type = (offer.effect_type or "balance_bonus").lower()
|
||||
effect_type = (offer.effect_type or "percent_discount").lower()
|
||||
|
||||
if effect_type == "test_access":
|
||||
success, added_squads, expires_at, error_code = await promo_offer_service.grant_test_access(
|
||||
@@ -5093,34 +5207,36 @@ async def claim_discount_offer(
|
||||
await callback.answer(popup_text, show_alert=True)
|
||||
await callback.message.answer(success_message)
|
||||
return
|
||||
elif effect_type in {"percent_discount", "balance_bonus"}:
|
||||
percent = max(0, offer.discount_percent or 0)
|
||||
|
||||
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
|
||||
db_user.pending_discount_percent = percent
|
||||
db_user.pending_discount_expires_at = offer.expires_at
|
||||
db_user.pending_discount_offer_id = offer.id
|
||||
|
||||
await mark_offer_claimed(db, offer)
|
||||
await mark_offer_claimed(db, offer)
|
||||
await db.refresh(db_user)
|
||||
|
||||
success_message = texts.get(
|
||||
"DISCOUNT_CLAIM_SUCCESS",
|
||||
"🎉 Скидка {percent}% активирована! На баланс начислено {amount}.",
|
||||
).format(
|
||||
percent=offer.discount_percent,
|
||||
amount=settings.format_price(bonus_amount),
|
||||
)
|
||||
success_message = texts.get(
|
||||
"DISCOUNT_CLAIM_SUCCESS",
|
||||
"🎉 Скидка {percent}% активирована! Она автоматически применится при оплате подписки.",
|
||||
).format(percent=percent)
|
||||
|
||||
await callback.answer("✅ Скидка активирована!", show_alert=True)
|
||||
await callback.message.answer(success_message)
|
||||
if offer.expires_at:
|
||||
expires_text = offer.expires_at.strftime("%d.%m.%Y %H:%M")
|
||||
expiry_note = texts.get(
|
||||
"DISCOUNT_CLAIM_EXPIRY_NOTE",
|
||||
"⏳ Скидка действует до {expires_at}.",
|
||||
).format(expires_at=expires_text)
|
||||
success_message = f"{success_message}\n{expiry_note}"
|
||||
|
||||
await callback.answer(texts.get("DISCOUNT_CLAIM_POPUP", "✅ Скидка активирована!"), show_alert=True)
|
||||
await callback.message.answer(success_message)
|
||||
return
|
||||
|
||||
else:
|
||||
await mark_offer_claimed(db, offer)
|
||||
await callback.answer(texts.get("DISCOUNT_CLAIM_POPUP", "✅ Скидка активирована!"), show_alert=True)
|
||||
|
||||
|
||||
async def handle_device_guide(
|
||||
|
||||
@@ -548,11 +548,16 @@
|
||||
"CREATE_TICKET_BUTTON": "🎫 Create ticket",
|
||||
"DELETE_MESSAGE": "🗑 Delete",
|
||||
"DISCOUNT_BONUS_DESCRIPTION": "Renewal discount bonus",
|
||||
"SUBSCRIPTION_EXTRA_DISCOUNT_LINE": "- Extra offer discount: -{amount} ({percent}%).",
|
||||
"SUBSCRIPTION_TOTAL_WITH_EXTRA_DISCOUNT": " (includes extra {percent}% discount: -{amount})",
|
||||
"SUBSCRIPTION_EXTRA_DISCOUNT_NOTE": "⏳ Extra discount valid until {expires_at}.",
|
||||
"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_SUCCESS": "🎉 Discount of {percent}% activated! {amount} credited to your balance.",
|
||||
"DISCOUNT_CLAIM_SUCCESS": "🎉 Discount of {percent}% activated! It will be applied automatically at checkout.",
|
||||
"DISCOUNT_CLAIM_EXPIRY_NOTE": "⏳ Discount 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 +597,8 @@
|
||||
"REPORT_CLOSE_ERROR": "❌ Failed to close the report.",
|
||||
"SENDING_ATTACHMENTS": "📎 Sending attachments...",
|
||||
"SUBSCRIPTION_EXPIRED_1D": "⛔ <b>Your subscription expired</b>\n\nAccess was disabled on {end_date}. Renew to return to the service.\n\n💎 Renewal price: {price}",
|
||||
"SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 <b>{percent}% discount on renewal</b>\n\nTap “Get discount” and we'll add {bonus} to your balance. The offer is valid until {expires_at}.",
|
||||
"SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 <b>Personal {percent}% discount</b>\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": "🔥 <b>{percent}% renewal discount</b>\n\nActivate the offer — an extra {percent}% discount will be applied automatically at checkout. It stacks with your promo group and is valid until {expires_at}.",
|
||||
"SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 <b>Personal {percent}% discount</b>\n\nIt's been {trigger_days} days without a subscription — come back and an extra {percent}% discount will be applied automatically at checkout. It stacks with your promo group and is valid until {expires_at}.",
|
||||
"SUBSCRIPTION_EXTEND": "💎 Extend subscription",
|
||||
"SUBSCRIPTION_HAPP_CRYPTOLINK_BLOCK": "<blockquote expandable><code>{crypto_link}</code></blockquote>",
|
||||
"SUBSCRIPTION_HAPP_LINK_PROMPT": "🔒 Subscription link is ready. Tap the \"Connect\" button below to open it in Happ.",
|
||||
|
||||
@@ -548,11 +548,16 @@
|
||||
"CREATE_TICKET_BUTTON": "🎫 Создать тикет",
|
||||
"DELETE_MESSAGE": "🗑 Удалить",
|
||||
"DISCOUNT_BONUS_DESCRIPTION": "Скидка за продление подписки",
|
||||
"SUBSCRIPTION_EXTRA_DISCOUNT_LINE": "- Доп. скидка по предложению: -{amount} ({percent}%).",
|
||||
"SUBSCRIPTION_TOTAL_WITH_EXTRA_DISCOUNT": " (включая доп. скидку {percent}%: -{amount})",
|
||||
"SUBSCRIPTION_EXTRA_DISCOUNT_NOTE": "⏳ Доп. скидка действует до {expires_at}.",
|
||||
"DISCOUNT_CLAIM_ALREADY": "ℹ️ Скидка уже была активирована ранее.",
|
||||
"DISCOUNT_CLAIM_ERROR": "❌ Не удалось начислить скидку. Попробуйте позже.",
|
||||
"DISCOUNT_CLAIM_EXPIRED": "⚠️ Время действия предложения истекло.",
|
||||
"DISCOUNT_CLAIM_NOT_FOUND": "❌ Предложение не найдено.",
|
||||
"DISCOUNT_CLAIM_SUCCESS": "🎉 Скидка {percent}% активирована! На баланс начислено {amount}.",
|
||||
"DISCOUNT_CLAIM_SUCCESS": "🎉 Скидка {percent}% активирована! Она автоматически применится при оплате подписки.",
|
||||
"DISCOUNT_CLAIM_EXPIRY_NOTE": "⏳ Скидка действует до {expires_at}.",
|
||||
"DISCOUNT_CLAIM_POPUP": "✅ Скидка активирована!",
|
||||
"ENTER_BLOCK_MINUTES": "Введите количество минут для блокировки пользователя (например, 15):",
|
||||
"LANGUAGE_SELECTION_DISABLED": "⚙️ Выбор языка временно недоступен. Используем язык по умолчанию.",
|
||||
"MARK_AS_ANSWERED": "✅ Отметить как отвеченный",
|
||||
@@ -592,8 +597,8 @@
|
||||
"REPORT_CLOSE_ERROR": "❌ Не удалось закрыть отчет.",
|
||||
"SENDING_ATTACHMENTS": "📎 Отправляю вложения...",
|
||||
"SUBSCRIPTION_EXPIRED_1D": "⛔ <b>Подписка закончилась</b>\n\nДоступ был отключён {end_date}. Продлите подписку, чтобы вернуть полный доступ.\n\n💎 Стоимость продления: {price}",
|
||||
"SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 <b>Скидка {percent}% на продление</b>\n\nНажмите «Получить скидку», и мы начислим {bonus} на ваш баланс. Предложение действительно до {expires_at}.",
|
||||
"SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 <b>Индивидуальная скидка {percent}%</b>\n\nПрошло {trigger_days} дней без подписки. Вернитесь — нажмите «Получить скидку», и {bonus} поступит на баланс. Предложение действительно до {expires_at}.",
|
||||
"SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 <b>Скидка {percent}% на продление</b>\n\nАктивируйте предложение — дополнительная скидка {percent}% автоматически применится при оплате. Скидка суммируется с вашей промогруппой и действует до {expires_at}.",
|
||||
"SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 <b>Индивидуальная скидка {percent}%</b>\n\nПрошло {trigger_days} дней без подписки — возвращайтесь, и скидка {percent}% автоматически применится при оплате. Скидка суммируется с вашей промогруппой и действует до {expires_at}.",
|
||||
"SUBSCRIPTION_EXTEND": "💎 Продлить подписку",
|
||||
"SUBSCRIPTION_HAPP_CRYPTOLINK_BLOCK": "<blockquote expandable><code>{crypto_link}</code></blockquote>",
|
||||
"SUBSCRIPTION_HAPP_LINK_PROMPT": "🔒 Ссылка на подписку создана. Нажмите кнопку \"Подключиться\" ниже, чтобы открыть её в Happ.",
|
||||
|
||||
@@ -681,14 +681,13 @@ 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,
|
||||
bonus_amount_kopeks=0,
|
||||
valid_hours=valid_hours,
|
||||
)
|
||||
success = await self._send_expired_discount_notification(
|
||||
@@ -698,7 +697,6 @@ class MonitoringService:
|
||||
offer.expires_at,
|
||||
offer.id,
|
||||
"second",
|
||||
bonus_amount,
|
||||
)
|
||||
if success:
|
||||
await record_notification(db, user.id, subscription.id, "expired_discount_wave2")
|
||||
@@ -711,14 +709,13 @@ 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,
|
||||
bonus_amount_kopeks=0,
|
||||
valid_hours=valid_hours,
|
||||
)
|
||||
success = await self._send_expired_discount_notification(
|
||||
@@ -728,7 +725,6 @@ class MonitoringService:
|
||||
offer.expires_at,
|
||||
offer.id,
|
||||
"third",
|
||||
bonus_amount,
|
||||
trigger_days=trigger_days,
|
||||
)
|
||||
if success:
|
||||
@@ -1191,7 +1187,6 @@ class MonitoringService:
|
||||
expires_at: datetime,
|
||||
offer_id: int,
|
||||
wave: str,
|
||||
bonus_amount: int,
|
||||
trigger_days: int = None,
|
||||
) -> bool:
|
||||
try:
|
||||
@@ -1202,8 +1197,8 @@ class MonitoringService:
|
||||
"SUBSCRIPTION_EXPIRED_SECOND_WAVE",
|
||||
(
|
||||
"🔥 <b>Скидка {percent}% на продление</b>\n\n"
|
||||
"Нажмите «Получить скидку», и мы начислим {bonus} на баланс. "
|
||||
"Предложение действует до {expires_at}."
|
||||
"Активируйте предложение — дополнительная скидка {percent}% автоматически применится при оплате. "
|
||||
"Скидка суммируется с вашей промогруппой и действует до {expires_at}."
|
||||
),
|
||||
)
|
||||
else:
|
||||
@@ -1211,14 +1206,13 @@ class MonitoringService:
|
||||
"SUBSCRIPTION_EXPIRED_THIRD_WAVE",
|
||||
(
|
||||
"🎁 <b>Индивидуальная скидка {percent}%</b>\n\n"
|
||||
"Прошло {trigger_days} дней без подписки — возвращайтесь, и мы добавим {bonus} на баланс. "
|
||||
"Скидка действует до {expires_at}."
|
||||
"Прошло {trigger_days} дней без подписки — возвращайтесь, и скидка {percent}% автоматически применится при оплате. "
|
||||
"Скидка суммируется с вашей промогруппой и действует до {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 "",
|
||||
)
|
||||
|
||||
@@ -527,9 +527,11 @@
|
||||
"TRIAL_INACTIVE_1H": "⏳ <b>An hour has passed and we haven't seen any traffic yet</b>\n\nOpen the connection guide and follow the steps. We're always ready to help!",
|
||||
"TRIAL_INACTIVE_24H": "⏳ <b>A full day passed without activity</b>\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": "⛔ <b>Your subscription expired</b>\n\nAccess was disabled on {end_date}. Renew to return to the service.\n\n💎 Renewal price: {price}",
|
||||
"SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 <b>{percent}% discount on renewal</b>\n\nTap “Get discount” and we'll add {bonus} to your balance. The offer is valid until {expires_at}.",
|
||||
"SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 <b>Personal {percent}% discount</b>\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": "🔥 <b>{percent}% renewal discount</b>\n\nActivate the offer — an extra {percent}% discount will be applied automatically at checkout. It stacks with your promo group and is valid until {expires_at}.",
|
||||
"SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 <b>Personal {percent}% discount</b>\n\nIt's been {trigger_days} days without a subscription — come back and an extra {percent}% discount will be applied automatically at checkout. It stacks with your promo group and is valid until {expires_at}.",
|
||||
"DISCOUNT_CLAIM_SUCCESS": "🎉 Discount of {percent}% activated! It will be applied automatically at checkout.",
|
||||
"DISCOUNT_CLAIM_EXPIRY_NOTE": "⏳ Discount valid until {expires_at}.",
|
||||
"DISCOUNT_CLAIM_POPUP": "✅ Discount activated!",
|
||||
"DISCOUNT_CLAIM_ALREADY": "ℹ️ This discount has already been activated.",
|
||||
"DISCOUNT_CLAIM_EXPIRED": "⚠️ The offer has expired.",
|
||||
"DISCOUNT_CLAIM_NOT_FOUND": "❌ Offer not found.",
|
||||
@@ -540,6 +542,9 @@
|
||||
"TEST_ACCESS_ACTIVATED_MESSAGE": "🎉 Test servers are connected! Access is active until {expires_at}.",
|
||||
"TEST_ACCESS_ACTIVATED_POPUP": "✅ Access granted!",
|
||||
"DISCOUNT_BONUS_DESCRIPTION": "Renewal discount bonus",
|
||||
"SUBSCRIPTION_EXTRA_DISCOUNT_LINE": "- Extra offer discount: -{amount} ({percent}%).",
|
||||
"SUBSCRIPTION_TOTAL_WITH_EXTRA_DISCOUNT": " (includes extra {percent}% discount: -{amount})",
|
||||
"SUBSCRIPTION_EXTRA_DISCOUNT_NOTE": "⏳ Extra discount valid until {expires_at}.",
|
||||
"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):",
|
||||
|
||||
@@ -527,9 +527,11 @@
|
||||
"TRIAL_INACTIVE_1H": "⏳ <b>Прошёл час, а подключение не выполнено</b>\n\nЕсли возникли сложности — откройте инструкцию и следуйте шагам. Мы всегда готовы помочь!",
|
||||
"TRIAL_INACTIVE_24H": "⏳ <b>Прошли сутки с начала теста</b>\n\nМы не видим трафика по вашей подписке. Загляните в инструкцию или напишите в поддержку — поможем подключиться!",
|
||||
"SUBSCRIPTION_EXPIRED_1D": "⛔ <b>Подписка закончилась</b>\n\nДоступ был отключён {end_date}. Продлите подписку, чтобы вернуть полный доступ.\n\n💎 Стоимость продления: {price}",
|
||||
"SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 <b>Скидка {percent}% на продление</b>\n\nНажмите «Получить скидку», и мы начислим {bonus} на ваш баланс. Предложение действительно до {expires_at}.",
|
||||
"SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 <b>Индивидуальная скидка {percent}%</b>\n\nПрошло {trigger_days} дней без подписки. Вернитесь — нажмите «Получить скидку», и {bonus} поступит на баланс. Предложение действительно до {expires_at}.",
|
||||
"DISCOUNT_CLAIM_SUCCESS": "🎉 Скидка {percent}% активирована! На баланс начислено {amount}.",
|
||||
"SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 <b>Скидка {percent}% на продление</b>\n\nАктивируйте предложение — дополнительная скидка {percent}% автоматически применится при оплате. Скидка суммируется с вашей промогруппой и действует до {expires_at}.",
|
||||
"SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 <b>Индивидуальная скидка {percent}%</b>\n\nПрошло {trigger_days} дней без подписки — возвращайтесь, и скидка {percent}% автоматически применится при оплате. Скидка суммируется с вашей промогруппой и действует до {expires_at}.",
|
||||
"DISCOUNT_CLAIM_SUCCESS": "🎉 Скидка {percent}% активирована! Она автоматически применится при оплате подписки.",
|
||||
"DISCOUNT_CLAIM_EXPIRY_NOTE": "⏳ Скидка действует до {expires_at}.",
|
||||
"DISCOUNT_CLAIM_POPUP": "✅ Скидка активирована!",
|
||||
"DISCOUNT_CLAIM_ALREADY": "ℹ️ Скидка уже была активирована ранее.",
|
||||
"DISCOUNT_CLAIM_EXPIRED": "⚠️ Время действия предложения истекло.",
|
||||
"DISCOUNT_CLAIM_NOT_FOUND": "❌ Предложение не найдено.",
|
||||
@@ -540,6 +542,9 @@
|
||||
"TEST_ACCESS_ACTIVATED_MESSAGE": "🎉 Тестовые сервера подключены! Доступ активен до {expires_at}.",
|
||||
"TEST_ACCESS_ACTIVATED_POPUP": "✅ Доступ выдан!",
|
||||
"DISCOUNT_BONUS_DESCRIPTION": "Скидка за продление подписки",
|
||||
"SUBSCRIPTION_EXTRA_DISCOUNT_LINE": "- Доп. скидка по предложению: -{amount} ({percent}%).",
|
||||
"SUBSCRIPTION_TOTAL_WITH_EXTRA_DISCOUNT": " (включая доп. скидку {percent}%: -{amount})",
|
||||
"SUBSCRIPTION_EXTRA_DISCOUNT_NOTE": "⏳ Доп. скидка действует до {expires_at}.",
|
||||
"NOTIFICATION_VALUE_INVALID": "❌ Некорректное значение, укажите число.",
|
||||
"NOTIFICATION_VALUE_UPDATED": "✅ Настройки обновлены.",
|
||||
"NOTIFY_PROMPT_SECOND_PERCENT": "Введите новый процент скидки для уведомления через 2-3 дня (0-100):",
|
||||
|
||||
Reference in New Issue
Block a user