diff --git a/.env.example b/.env.example
index 2ad25e03..6387650a 100644
--- a/.env.example
+++ b/.env.example
@@ -85,8 +85,6 @@ REMNAWAVE_USER_DELETE_MODE=delete
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_LIMIT_GB=10
TRIAL_DEVICE_LIMIT=1
-TRIAL_PAYMENT_ENABLED=false
-TRIAL_ACTIVATION_PRICE=0
TRIAL_SQUAD_UUID=
# ===== ПЛАТНАЯ ПОДПИСКА =====
diff --git a/README.md b/README.md
index 8983a19a..06f7670b 100644
--- a/README.md
+++ b/README.md
@@ -538,12 +538,6 @@ TRAFFIC_PACKAGES_CONFIG="100:15000:true"
# Бесплатные устройства в триал подписке
TRIAL_DEVICE_LIMIT=1
-# Требовать оплату за активацию триала
-TRIAL_PAYMENT_ENABLED=false
-
-# Стоимость активации триала (в копейках)
-TRIAL_ACTIVATION_PRICE=0
-
# Бесплатные устройства в платной подписке
DEFAULT_DEVICE_LIMIT=3
diff --git a/app/config.py b/app/config.py
index 57549cca..0d63adaa 100644
--- a/app/config.py
+++ b/app/config.py
@@ -85,8 +85,6 @@ class Settings(BaseSettings):
TRIAL_TRAFFIC_LIMIT_GB: int = 10
TRIAL_DEVICE_LIMIT: int = 2
TRIAL_ADD_REMAINING_DAYS_TO_PAID: bool = False
- TRIAL_PAYMENT_ENABLED: bool = False
- TRIAL_ACTIVATION_PRICE: int = 0
DEFAULT_TRAFFIC_LIMIT_GB: int = 100
DEFAULT_DEVICE_LIMIT: int = 1
TRIAL_SQUAD_UUID: Optional[str] = None
@@ -885,24 +883,6 @@ class Settings(BaseSettings):
def get_disabled_mode_device_limit(self) -> Optional[int]:
return self.get_devices_selection_disabled_amount()
-
- def is_trial_paid_activation_enabled(self) -> bool:
- return bool(self.TRIAL_PAYMENT_ENABLED)
-
- def get_trial_activation_price(self) -> int:
- try:
- value = int(self.TRIAL_ACTIVATION_PRICE)
- except (TypeError, ValueError):
- logger.warning(
- "Некорректное значение TRIAL_ACTIVATION_PRICE: %s",
- self.TRIAL_ACTIVATION_PRICE,
- )
- return 0
-
- if value < 0:
- return 0
-
- return value
def is_yookassa_enabled(self) -> bool:
return (self.YOOKASSA_ENABLED and
diff --git a/app/handlers/admin/pricing.py b/app/handlers/admin/pricing.py
index eae7b8c9..c3abf75d 100644
--- a/app/handlers/admin/pricing.py
+++ b/app/handlers/admin/pricing.py
@@ -96,24 +96,6 @@ TRIAL_ENTRIES: Tuple[SettingEntry, ...] = (
label_en="📱 Device limit",
action="input",
),
- SettingEntry(
- key="TRIAL_PAYMENT_ENABLED",
- section="trial",
- label_ru="💳 Платная активация",
- label_en="💳 Paid activation",
- action="toggle",
- description_ru="Если включено — за активацию триала будет списываться указанная сумма.",
- description_en="When enabled, the configured amount is charged during trial activation.",
- ),
- SettingEntry(
- key="TRIAL_ACTIVATION_PRICE",
- section="trial",
- label_ru="💰 Стоимость активации",
- label_en="💰 Activation price",
- action="price",
- description_ru="Указывается в копейках. 0 — бесплатная активация.",
- description_en="Amount in kopeks. 0 — free activation.",
- ),
SettingEntry(
key="TRIAL_ADD_REMAINING_DAYS_TO_PAID",
section="trial",
@@ -308,14 +290,11 @@ def _format_trial_summary(lang_code: str) -> str:
duration = settings.TRIAL_DURATION_DAYS
traffic = settings.TRIAL_TRAFFIC_LIMIT_GB
devices = settings.TRIAL_DEVICE_LIMIT
- price_note = ""
- if settings.is_trial_paid_activation_enabled():
- price_note = f", 💳 {settings.format_price(settings.get_trial_activation_price())}"
traffic_label = _format_traffic_label(traffic, lang_code, short=True)
devices_label = f"{devices}📱" if lang_code == "ru" else f"{devices}📱"
days_suffix = "д" if lang_code == "ru" else "d"
- return f"{duration}{days_suffix}, {traffic_label}, {devices_label}{price_note}"
+ return f"{duration}{days_suffix}, {traffic_label}, {devices_label}"
def _format_core_summary(lang_code: str) -> str:
diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py
index c67b8f4a..2ea0ebfa 100644
--- a/app/handlers/subscription/purchase.py
+++ b/app/handlers/subscription/purchase.py
@@ -58,13 +58,6 @@ from app.services.subscription_checkout_service import (
should_offer_checkout_resume,
)
from app.services.subscription_service import SubscriptionService
-from app.services.trial_activation_service import (
- TrialPaymentChargeFailed,
- TrialPaymentInsufficientFunds,
- charge_trial_activation_if_required,
- rollback_trial_subscription_activation,
- preview_trial_activation_charge,
-)
def _serialize_markup(markup: Optional[InlineKeyboardMarkup]) -> Optional[Any]:
@@ -458,22 +451,12 @@ async def show_trial_offer(
devices=trial_device_limit,
)
- price_line = ""
- if settings.is_trial_paid_activation_enabled():
- trial_price = settings.get_trial_activation_price()
- if trial_price > 0:
- price_line = texts.t(
- "TRIAL_PAYMENT_PRICE_LINE",
- "\n💳 Стоимость активации: {price}",
- ).format(price=settings.format_price(trial_price))
-
trial_text = texts.TRIAL_AVAILABLE.format(
days=settings.TRIAL_DURATION_DAYS,
traffic=texts.format_traffic(settings.TRIAL_TRAFFIC_LIMIT_GB),
devices=trial_device_limit if trial_device_limit is not None else "",
devices_line=devices_line,
- server_name=trial_server_name,
- price_line=price_line,
+ server_name=trial_server_name
)
await callback.message.edit_text(
@@ -499,28 +482,6 @@ async def activate_trial(
await callback.answer()
return
- try:
- preview_trial_activation_charge(db_user)
- except TrialPaymentInsufficientFunds as error:
- required_label = settings.format_price(error.required_amount)
- balance_label = settings.format_price(error.balance_amount)
- missing_label = settings.format_price(error.missing_amount)
- message = texts.t(
- "TRIAL_PAYMENT_INSUFFICIENT_FUNDS",
- "⚠️ Недостаточно средств для активации триала.\n"
- "Необходимо: {required}\nНа балансе: {balance}\n"
- "Не хватает: {missing}\n\nПополните баланс и попробуйте снова.",
- ).format(required=required_label, balance=balance_label, missing=missing_label)
-
- await callback.message.edit_text(
- message,
- reply_markup=get_insufficient_balance_keyboard(db_user.language),
- )
- await callback.answer()
- return
-
- charged_amount = 0
-
try:
forced_devices = None
if not settings.is_devices_selection_enabled():
@@ -534,72 +495,6 @@ async def activate_trial(
await db.refresh(db_user)
- try:
- charged_amount = await charge_trial_activation_if_required(
- db,
- db_user,
- description="Активация триала через бота",
- )
- except TrialPaymentInsufficientFunds as error:
- rollback_success = await rollback_trial_subscription_activation(db, subscription)
- await db.refresh(db_user)
- if not rollback_success:
- await callback.answer(
- texts.t(
- "TRIAL_ROLLBACK_FAILED",
- "Не удалось отменить активацию триала. Попробуйте позже.",
- ),
- show_alert=True,
- )
- return
-
- logger.error(
- "Insufficient funds detected after trial creation for user %s: %s",
- db_user.id,
- error,
- )
- required_label = settings.format_price(error.required_amount)
- balance_label = settings.format_price(error.balance_amount)
- missing_label = settings.format_price(error.missing_amount)
- message = texts.t(
- "TRIAL_PAYMENT_INSUFFICIENT_FUNDS",
- "⚠️ Недостаточно средств для активации триала.\n"
- "Необходимо: {required}\nНа балансе: {balance}\n"
- "Не хватает: {missing}\n\nПополните баланс и попробуйте снова.",
- ).format(
- required=required_label,
- balance=balance_label,
- missing=missing_label,
- )
-
- await callback.message.edit_text(
- message,
- reply_markup=get_insufficient_balance_keyboard(db_user.language),
- )
- await callback.answer()
- return
- except TrialPaymentChargeFailed:
- rollback_success = await rollback_trial_subscription_activation(db, subscription)
- await db.refresh(db_user)
- if not rollback_success:
- await callback.answer(
- texts.t(
- "TRIAL_ROLLBACK_FAILED",
- "Не удалось отменить активацию триала. Попробуйте позже.",
- ),
- show_alert=True,
- )
- return
-
- await callback.answer(
- texts.t(
- "TRIAL_PAYMENT_FAILED",
- "Не удалось списать средства для активации триала. Попробуйте позже.",
- ),
- show_alert=True,
- )
- return
-
subscription_service = SubscriptionService()
remnawave_user = await subscription_service.create_remnawave_user(
db, subscription
@@ -609,51 +504,39 @@ async def activate_trial(
try:
notification_service = AdminNotificationService(callback.bot)
- await notification_service.send_trial_activation_notification(
- db,
- db_user,
- subscription,
- charged_amount_kopeks=charged_amount,
- )
+ await notification_service.send_trial_activation_notification(db, db_user, subscription)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о триале: {e}")
subscription_link = get_display_subscription_link(subscription)
hide_subscription_link = settings.should_hide_subscription_link()
- payment_note = ""
- if charged_amount > 0:
- payment_note = "\n\n" + texts.t(
- "TRIAL_PAYMENT_CHARGED_NOTE",
- "💳 С вашего баланса списано {amount}.",
- ).format(amount=settings.format_price(charged_amount))
-
if remnawave_user and subscription_link:
if settings.is_happ_cryptolink_mode():
trial_success_text = (
- f"{texts.TRIAL_ACTIVATED}\n\n"
- + texts.t(
- "SUBSCRIPTION_HAPP_LINK_PROMPT",
- "🔒 Ссылка на подписку создана. Нажмите кнопку \"Подключиться\" ниже, чтобы открыть её в Happ.",
- )
- + "\n\n"
- + texts.t(
- "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
- "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
- )
+ f"{texts.TRIAL_ACTIVATED}\n\n"
+ + texts.t(
+ "SUBSCRIPTION_HAPP_LINK_PROMPT",
+ "🔒 Ссылка на подписку создана. Нажмите кнопку \"Подключиться\" ниже, чтобы открыть её в Happ.",
+ )
+ + "\n\n"
+ + texts.t(
+ "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
+ "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
+ )
)
elif hide_subscription_link:
trial_success_text = (
- f"{texts.TRIAL_ACTIVATED}\n\n"
- + texts.t(
- "SUBSCRIPTION_LINK_HIDDEN_NOTICE",
- "ℹ️ Ссылка подписки доступна по кнопкам ниже или в разделе \"Моя подписка\".",
- )
- + "\n\n"
- + texts.t(
- "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
- "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
- )
+ f"{texts.TRIAL_ACTIVATED}\n\n"
+ + texts.t(
+ "SUBSCRIPTION_LINK_HIDDEN_NOTICE",
+ "ℹ️ Ссылка подписки доступна по кнопкам ниже или в разделе \"Моя подписка\".",
+ )
+ + "\n\n"
+ + texts.t(
+ "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
+ "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
+ )
)
else:
subscription_import_link = texts.t(
@@ -667,8 +550,6 @@ async def activate_trial(
f"{texts.t('SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT', '📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве')}"
)
- trial_success_text += payment_note
-
connect_mode = settings.CONNECT_BUTTON_MODE
if connect_mode == "miniapp_subscription":
@@ -679,12 +560,8 @@ async def activate_trial(
web_app=types.WebAppInfo(url=subscription_link),
)
],
- [
- InlineKeyboardButton(
- text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
- callback_data="back_to_menu",
- )
- ],
+ [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu")],
])
elif connect_mode == "miniapp_custom":
if not settings.MINIAPP_CUSTOM_URL:
@@ -704,33 +581,22 @@ async def activate_trial(
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL),
)
],
- [
- InlineKeyboardButton(
- text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
- callback_data="back_to_menu",
- )
- ],
+ [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu")],
])
elif connect_mode == "link":
rows = [
- [
- InlineKeyboardButton(
- text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
- url=subscription_link,
- )
- ]
+ [InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), url=subscription_link)]
]
happ_row = get_happ_download_button_row(texts)
if happ_row:
rows.append(happ_row)
- rows.append(
- [
- InlineKeyboardButton(
- text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
- callback_data="back_to_menu",
- )
- ]
- )
+ rows.append([
+ InlineKeyboardButton(
+ text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu"
+ )
+ ])
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
elif connect_mode == "happ_cryptolink":
rows = [
@@ -744,51 +610,33 @@ async def activate_trial(
happ_row = get_happ_download_button_row(texts)
if happ_row:
rows.append(happ_row)
- rows.append(
- [
- InlineKeyboardButton(
- text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
- callback_data="back_to_menu",
- )
- ]
- )
+ rows.append([
+ InlineKeyboardButton(
+ text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu"
+ )
+ ])
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
else:
- connect_keyboard = InlineKeyboardMarkup(
- inline_keyboard=[
- [
- InlineKeyboardButton(
- text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
- callback_data="subscription_connect",
- )
- ],
- [
- InlineKeyboardButton(
- text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
- callback_data="back_to_menu",
- )
- ],
- ]
- )
+ connect_keyboard = InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
+ callback_data="subscription_connect")],
+ [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu")],
+ ])
await callback.message.edit_text(
trial_success_text,
reply_markup=connect_keyboard,
- parse_mode="HTML",
+ parse_mode="HTML"
)
else:
- trial_success_text = (
- f"{texts.TRIAL_ACTIVATED}\n\n⚠️ Ссылка генерируется, попробуйте перейти в раздел 'Моя подписка' через несколько секунд."
- )
- trial_success_text += payment_note
await callback.message.edit_text(
- trial_success_text,
- reply_markup=get_back_keyboard(db_user.language),
+ f"{texts.TRIAL_ACTIVATED}\n\n⚠️ Ссылка генерируется, попробуйте перейти в раздел 'Моя подписка' через несколько секунд.",
+ reply_markup=get_back_keyboard(db_user.language)
)
- logger.info(
- f"✅ Активирована тестовая подписка для пользователя {db_user.telegram_id}"
- )
+ logger.info(f"✅ Активирована тестовая подписка для пользователя {db_user.telegram_id}")
except Exception as e:
logger.error(f"Ошибка активации триала: {e}")
diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json
index e1f95605..7e6655ac 100644
--- a/app/localization/locales/en.json
+++ b/app/localization/locales/en.json
@@ -1413,13 +1413,8 @@
"TRIAL_ACTIVATED": "🎉 Trial subscription activated!",
"TRIAL_ACTIVATE_BUTTON": "🎁 Activate",
"TRIAL_ALREADY_USED": "❌ The trial subscription has already been used",
- "TRIAL_AVAILABLE": "\n🎁 Trial subscription\n\nYou can get a trial plan{price_line}:\n\n⏰ Duration: {days} days\n📈 Traffic: {traffic}{devices_line}\n🌍 Server: {server_name}\n\nActivate the trial subscription?\n",
+ "TRIAL_AVAILABLE": "\n🎁 Trial subscription\n\nYou can get a free trial plan:\n\n⏰ Duration: {days} days\n📈 Traffic: {traffic}{devices_line}\n🌍 Server: {server_name}\n\nActivate the trial subscription?\n",
"TRIAL_AVAILABLE_DEVICES_LINE": "\n📱 Devices: {devices} pcs",
- "TRIAL_PAYMENT_PRICE_LINE": "\n💳 Activation price: {price}",
- "TRIAL_PAYMENT_INSUFFICIENT_FUNDS": "⚠️ Not enough funds to activate the trial.\nRequired: {required}\nOn balance: {balance}\nMissing: {missing}\n\nTop up your balance and try again.",
- "TRIAL_PAYMENT_FAILED": "We couldn't charge your balance to activate the trial. Please try again later.",
- "TRIAL_ROLLBACK_FAILED": "We couldn't cancel the trial activation after a payment error. Please contact support and try again later.",
- "TRIAL_PAYMENT_CHARGED_NOTE": "💳 {amount} has been deducted from your balance.",
"TRIAL_CHANNEL_UNSUBSCRIBED": "\n🚫 Access paused\n\nWe couldn't find your subscription to our channel, so the trial plan has been disabled.\n\nJoin the channel and tap “{check_button}” to restore access.",
"TRIAL_ENDING_SOON": "\n🎁 The trial subscription is ending soon!\n\nYour trial expires in a few hours.\n\n💎 Don't want to lose VPN access?\nSwitch to the full subscription!\n\n🔥 Special offer:\n• 30 days for {price}\n• Unlimited traffic\n• All servers available\n• Speeds up to 1 Gbit/s\n\n⚡️ Activate before the trial ends!\n",
"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!",
diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json
index c0afb67e..cee78825 100644
--- a/app/localization/locales/ru.json
+++ b/app/localization/locales/ru.json
@@ -1433,13 +1433,8 @@
"TRIAL_ACTIVATED": "🎉 Тестовая подписка активирована!",
"TRIAL_ACTIVATE_BUTTON": "🎁 Активировать",
"TRIAL_ALREADY_USED": "❌ Тестовая подписка уже была использована",
- "TRIAL_AVAILABLE": "\n🎁 Тестовая подписка\n\nВы можете получить тестовую подписку{price_line}:\n\n⏰ Период: {days} дней\n📈 Трафик: {traffic}{devices_line}\n🌍 Сервер: {server_name}\n\nАктивировать тестовую подписку?\n",
+ "TRIAL_AVAILABLE": "\n🎁 Тестовая подписка\n\nВы можете получить бесплатную тестовую подписку:\n\n⏰ Период: {days} дней\n📈 Трафик: {traffic}{devices_line}\n🌍 Сервер: {server_name}\n\nАктивировать тестовую подписку?\n",
"TRIAL_AVAILABLE_DEVICES_LINE": "\n📱 Устройства: {devices} шт.",
- "TRIAL_PAYMENT_PRICE_LINE": "\n💳 Стоимость активации: {price}",
- "TRIAL_PAYMENT_INSUFFICIENT_FUNDS": "⚠️ Недостаточно средств для активации триала.\nНеобходимо: {required}\nНа балансе: {balance}\nНе хватает: {missing}\n\nПополните баланс и попробуйте снова.",
- "TRIAL_PAYMENT_FAILED": "Не удалось списать средства для активации триала. Попробуйте позже.",
- "TRIAL_ROLLBACK_FAILED": "Не удалось отменить активацию триала после ошибки списания. Свяжитесь с поддержкой и попробуйте позже.",
- "TRIAL_PAYMENT_CHARGED_NOTE": "💳 С вашего баланса списано {amount}.",
"TRIAL_CHANNEL_UNSUBSCRIBED": "\n🚫 Доступ приостановлен\n\nМы не нашли вашу подписку на наш канал, поэтому тестовая подписка отключена.\n\nПодпишитесь на канал и нажмите «{check_button}», чтобы вернуть доступ.",
"TRIAL_ENDING_SOON": "\n🎁 Тестовая подписка скоро закончится!\n\nВаша тестовая подписка истекает через несколько часов.\n\n💎 Не хотите остаться без VPN?\nПереходите на полную подписку!\n\n🔥 Специальное предложение:\n• 30 дней всего за {price}\n• Безлимитный трафик \n• Все серверы доступны\n• Скорость до 1ГБит/сек\n\n⚡️ Успейте оформить до окончания тестового периода!\n",
"TRIAL_INACTIVE_1H": "⏳ Прошёл час, а подключение не выполнено\n\nЕсли возникли сложности — откройте инструкцию и следуйте шагам. Мы всегда готовы помочь!",
diff --git a/app/services/admin_notification_service.py b/app/services/admin_notification_service.py
index 8b4c1379..553e794f 100644
--- a/app/services/admin_notification_service.py
+++ b/app/services/admin_notification_service.py
@@ -190,9 +190,7 @@ class AdminNotificationService:
self,
db: AsyncSession,
user: User,
- subscription: Subscription,
- *,
- charged_amount_kopeks: Optional[int] = None,
+ subscription: Subscription
) -> bool:
if not self._is_enabled():
return False
@@ -212,12 +210,6 @@ class AdminNotificationService:
else:
trial_device_limit = settings.TRIAL_DEVICE_LIMIT
- payment_block = ""
- if charged_amount_kopeks and charged_amount_kopeks > 0:
- payment_block = (
- f"\n💳 Оплата за активацию: {settings.format_price(charged_amount_kopeks)}"
- )
-
message = f"""🎯 АКТИВАЦИЯ ТРИАЛА
👤 Пользователь: {user_display}
@@ -232,7 +224,6 @@ class AdminNotificationService:
📊 Трафик: {self._format_traffic(settings.TRIAL_TRAFFIC_LIMIT_GB)}
📱 Устройства: {trial_device_limit}
🌐 Сервер: {subscription.connected_squads[0] if subscription.connected_squads else 'По умолчанию'}
-{payment_block}
📆 Действует до: {format_local_datetime(subscription.end_date, '%d.%m.%Y %H:%M')}
🔗 Реферер: {referrer_info}
diff --git a/app/services/trial_activation_service.py b/app/services/trial_activation_service.py
deleted file mode 100644
index 635a9611..00000000
--- a/app/services/trial_activation_service.py
+++ /dev/null
@@ -1,130 +0,0 @@
-from __future__ import annotations
-
-import logging
-from dataclasses import dataclass
-from typing import Optional
-
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from app.config import settings
-from app.database.crud.subscription import decrement_subscription_server_counts
-from app.database.crud.user import subtract_user_balance
-from app.database.models import Subscription, User
-
-
-logger = logging.getLogger(__name__)
-
-
-class TrialPaymentError(Exception):
- """Base exception for trial activation payment issues."""
-
-
-@dataclass(slots=True)
-class TrialPaymentInsufficientFunds(TrialPaymentError):
- required_amount: int
- balance_amount: int
-
- @property
- def missing_amount(self) -> int:
- return max(0, self.required_amount - self.balance_amount)
-
-
-class TrialPaymentChargeFailed(TrialPaymentError):
- """Raised when balance charge could not be completed."""
-
-
-def get_trial_activation_charge_amount() -> int:
- """Returns the configured activation charge in kopeks if payment is enabled."""
-
- if not settings.is_trial_paid_activation_enabled():
- return 0
-
- try:
- price_kopeks = int(settings.get_trial_activation_price() or 0)
- except (TypeError, ValueError): # pragma: no cover - defensive
- price_kopeks = 0
-
- return max(0, price_kopeks)
-
-
-def preview_trial_activation_charge(user: User) -> int:
- """Validates that the user can afford the trial activation charge."""
-
- price_kopeks = get_trial_activation_charge_amount()
- if price_kopeks <= 0:
- return 0
-
- balance = int(getattr(user, "balance_kopeks", 0) or 0)
- if balance < price_kopeks:
- raise TrialPaymentInsufficientFunds(price_kopeks, balance)
-
- return price_kopeks
-
-
-async def charge_trial_activation_if_required(
- db: AsyncSession,
- user: User,
- *,
- description: Optional[str] = None,
-) -> int:
- """Charges the user's balance if paid trial activation is enabled.
-
- Returns the charged amount in kopeks. If payment is not required or the
- configured price is zero, the function returns ``0``.
- """
-
- price_kopeks = preview_trial_activation_charge(user)
- if price_kopeks <= 0:
- return 0
-
- charge_description = description or "Активация триальной подписки"
-
- success = await subtract_user_balance(
- db,
- user,
- price_kopeks,
- charge_description,
- )
- if not success:
- raise TrialPaymentChargeFailed()
-
- # subtract_user_balance обновляет пользователя, но на всякий случай приводим к int
- return int(price_kopeks)
-
-
-async def rollback_trial_subscription_activation(
- db: AsyncSession,
- subscription: Optional[Subscription],
-) -> bool:
- """Attempts to undo a previously created trial subscription.
-
- Returns ``True`` when the rollback succeeds or when ``subscription`` is
- falsy. In case of a database failure the function returns ``False`` after
- logging the error so callers can decide how to proceed.
- """
-
- if not subscription:
- return True
-
- try:
- await decrement_subscription_server_counts(db, subscription)
- except Exception as error: # pragma: no cover - defensive logging
- logger.error(
- "Failed to decrement server counters during trial rollback for %s: %s",
- subscription.user_id,
- error,
- )
-
- try:
- await db.delete(subscription)
- await db.commit()
- except Exception as error: # pragma: no cover - defensive logging
- logger.error(
- "Failed to remove trial subscription %s after charge failure: %s",
- getattr(subscription, "id", ""),
- error,
- )
- await db.rollback()
- return False
-
- return True
diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py
index cdd49c65..e089968d 100644
--- a/app/webapi/routes/miniapp.py
+++ b/app/webapi/routes/miniapp.py
@@ -67,13 +67,6 @@ from app.services.payment_service import PaymentService, get_wata_payment_by_lin
from app.services.promo_offer_service import promo_offer_service
from app.services.promocode_service import PromoCodeService
from app.services.subscription_service import SubscriptionService
-from app.services.trial_activation_service import (
- TrialPaymentChargeFailed,
- TrialPaymentInsufficientFunds,
- charge_trial_activation_if_required,
- rollback_trial_subscription_activation,
- preview_trial_activation_charge,
-)
from app.services.subscription_purchase_service import (
purchase_service,
PurchaseBalanceError,
@@ -2895,13 +2888,6 @@ async def get_subscription_details(
trial_duration_days = (
settings.TRIAL_DURATION_DAYS if settings.TRIAL_DURATION_DAYS > 0 else None
)
- trial_price_kopeks = settings.get_trial_activation_price()
- trial_payment_required = (
- settings.is_trial_paid_activation_enabled() and trial_price_kopeks > 0
- )
- trial_price_label = (
- settings.format_price(trial_price_kopeks) if trial_payment_required else None
- )
subscription_missing_reason = None
if subscription is None:
@@ -2965,9 +2951,6 @@ async def get_subscription_details(
trial_available=trial_available,
trial_duration_days=trial_duration_days,
trial_status="available" if trial_available else "unavailable",
- trial_payment_required=trial_payment_required,
- trial_price_kopeks=trial_price_kopeks if trial_payment_required else None,
- trial_price_label=trial_price_label,
**autopay_extras,
)
@@ -3120,20 +3103,6 @@ async def activate_subscription_trial_endpoint(
},
)
- try:
- preview_trial_activation_charge(user)
- except TrialPaymentInsufficientFunds as error:
- missing = error.missing_amount
- raise HTTPException(
- status.HTTP_402_PAYMENT_REQUIRED,
- detail={
- "code": "insufficient_funds",
- "message": "Not enough funds to activate the trial",
- "missing_amount_kopeks": missing,
- "required_amount_kopeks": error.required_amount,
- "balance_kopeks": error.balance_amount,
- },
- ) from error
forced_devices = None
if not settings.is_devices_selection_enabled():
forced_devices = settings.get_disabled_mode_device_limit()
@@ -3158,61 +3127,6 @@ async def activate_subscription_trial_endpoint(
},
) from error
- charged_amount = 0
- try:
- charged_amount = await charge_trial_activation_if_required(db, user)
- except TrialPaymentInsufficientFunds as error:
- rollback_success = await rollback_trial_subscription_activation(db, subscription)
- await db.refresh(user)
- if not rollback_success:
- raise HTTPException(
- status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail={
- "code": "trial_rollback_failed",
- "message": "Failed to revert trial activation after charge error",
- },
- ) from error
-
- logger.error(
- "Balance check failed after trial creation for user %s: %s",
- user.id,
- error,
- )
- raise HTTPException(
- status.HTTP_402_PAYMENT_REQUIRED,
- detail={
- "code": "insufficient_funds",
- "message": "Not enough funds to activate the trial",
- "missing_amount_kopeks": error.missing_amount,
- "required_amount_kopeks": error.required_amount,
- "balance_kopeks": error.balance_amount,
- },
- ) from error
- except TrialPaymentChargeFailed as error:
- rollback_success = await rollback_trial_subscription_activation(db, subscription)
- await db.refresh(user)
- if not rollback_success:
- raise HTTPException(
- status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail={
- "code": "trial_rollback_failed",
- "message": "Failed to revert trial activation after charge error",
- },
- ) from error
-
- logger.error(
- "Failed to charge balance for trial activation after subscription %s creation: %s",
- subscription.id,
- error,
- )
- raise HTTPException(
- status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail={
- "code": "charge_failed",
- "message": "Failed to charge balance for trial activation",
- },
- ) from error
-
await db.refresh(user)
await db.refresh(subscription)
@@ -3244,9 +3158,6 @@ async def activate_subscription_trial_endpoint(
duration_days = settings.TRIAL_DURATION_DAYS
language_code = _normalize_language_code(user)
- charged_amount_label = (
- settings.format_price(charged_amount) if charged_amount > 0 else None
- )
if language_code == "ru":
if duration_days:
message = f"Триал активирован на {duration_days} дн. Приятного пользования!"
@@ -3258,19 +3169,8 @@ async def activate_subscription_trial_endpoint(
else:
message = "Trial activated successfully. Enjoy!"
- if charged_amount_label:
- if language_code == "ru":
- message = f"{message}\n\n💳 С вашего баланса списано {charged_amount_label}."
- else:
- message = f"{message}\n\n💳 {charged_amount_label} has been deducted from your balance."
-
await _with_admin_notification_service(
- lambda service: service.send_trial_activation_notification(
- db,
- user,
- subscription,
- charged_amount_kopeks=charged_amount,
- )
+ lambda service: service.send_trial_activation_notification(db, user, subscription)
)
return MiniAppSubscriptionTrialResponse(
@@ -3278,10 +3178,6 @@ async def activate_subscription_trial_endpoint(
subscription_id=getattr(subscription, "id", None),
trial_status="activated",
trial_duration_days=duration_days,
- charged_amount_kopeks=charged_amount if charged_amount > 0 else None,
- charged_amount_label=charged_amount_label,
- balance_kopeks=user.balance_kopeks,
- balance_label=settings.format_price(user.balance_kopeks),
)
diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py
index 839a6f17..c21b6060 100644
--- a/app/webapi/schemas/miniapp.py
+++ b/app/webapi/schemas/miniapp.py
@@ -465,11 +465,8 @@ class MiniAppSubscriptionResponse(BaseModel):
trial_available: bool = False
trial_duration_days: Optional[int] = None
trial_status: Optional[str] = None
- trial_payment_required: bool = Field(default=False, alias="trialPaymentRequired")
- trial_price_kopeks: Optional[int] = Field(default=None, alias="trialPriceKopeks")
- trial_price_label: Optional[str] = Field(default=None, alias="trialPriceLabel")
- model_config = ConfigDict(extra="allow", populate_by_name=True)
+ model_config = ConfigDict(extra="allow")
class MiniAppSubscriptionServerOption(BaseModel):
@@ -739,10 +736,6 @@ class MiniAppSubscriptionTrialResponse(BaseModel):
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
trial_status: Optional[str] = Field(default=None, alias="trialStatus")
trial_duration_days: Optional[int] = Field(default=None, alias="trialDurationDays")
- charged_amount_kopeks: Optional[int] = Field(default=None, alias="chargedAmountKopeks")
- charged_amount_label: Optional[str] = Field(default=None, alias="chargedAmountLabel")
- balance_kopeks: Optional[int] = Field(default=None, alias="balanceKopeks")
- balance_label: Optional[str] = Field(default=None, alias="balanceLabel")
model_config = ConfigDict(populate_by_name=True)