removed tribute

This commit is contained in:
machka pasla
2025-12-11 09:49:31 +03:00
parent 8eba23574b
commit 061fdeb72b
16 changed files with 24 additions and 606 deletions
+3 -6
View File
@@ -11,7 +11,7 @@
- **Пробная подписка:** Система пробных подписок для новых пользователей (активируется вручную по кнопке). - **Пробная подписка:** Система пробных подписок для новых пользователей (активируется вручную по кнопке).
- **Промокоды:** Возможность применять промокоды для получения скидок или бонусных дней. - **Промокоды:** Возможность применять промокоды для получения скидок или бонусных дней.
- **Реферальная программа:** Пользователи могут приглашать друзей и получать за это бонусные дни подписки. - **Реферальная программа:** Пользователи могут приглашать друзей и получать за это бонусные дни подписки.
- **Оплата:** Поддержка оплаты через YooKassa, FreeKassa (REST API), Platega, SeverPay, CryptoPay, Telegram Stars и Tribute. - **Оплата:** Поддержка оплаты через YooKassa, FreeKassa (REST API), Platega, SeverPay, CryptoPay и Telegram Stars.
### Для администраторов: ### Для администраторов:
- **Защищенная админ-панель:** Доступ только для администраторов, указанных в `ADMIN_IDS`. - **Защищенная админ-панель:** Доступ только для администраторов, указанных в `ADMIN_IDS`.
@@ -80,7 +80,7 @@
| `WEBHOOK_BASE_URL`| **Обязательно.** Базовый URL для вебхуков, например `https://your.domain.com`. | | `WEBHOOK_BASE_URL`| **Обязательно.** Базовый URL для вебхуков, например `https://your.domain.com`. |
| `WEB_SERVER_HOST` | Хост для веб-сервера. | `0.0.0.0` | | `WEB_SERVER_HOST` | Хост для веб-сервера. | `0.0.0.0` |
| `WEB_SERVER_PORT` | Порт для веб-сервера. | `8080` | | `WEB_SERVER_PORT` | Порт для веб-сервера. | `8080` |
| `PAYMENT_METHODS_ORDER` | (Опционально) Порядок отображения кнопок оплаты через запятую. Поддерживаемые ключи: `severpay`, `freekassa`, `platega`, `yookassa`, `tribute`, `stars`, `cryptopay`. Первый будет сверху. | | `PAYMENT_METHODS_ORDER` | (Опционально) Порядок отображения кнопок оплаты через запятую. Поддерживаемые ключи: `severpay`, `freekassa`, `platega`, `yookassa`, `stars`, `cryptopay`. Первый будет сверху. |
| `YOOKASSA_ENABLED` | Включить/выключить YooKassa (`true`/`false`). | | `YOOKASSA_ENABLED` | Включить/выключить YooKassa (`true`/`false`). |
| `YOOKASSA_SHOP_ID` | ID вашего магазина в YooKassa. | | `YOOKASSA_SHOP_ID` | ID вашего магазина в YooKassa. |
| `YOOKASSA_SECRET_KEY`| Секретный ключ магазина YooKassa. | | `YOOKASSA_SECRET_KEY`| Секретный ключ магазина YooKassa. |
@@ -96,7 +96,6 @@
| `FREEKASSA_PAYMENT_IP` | Внешний IP вашего сервера, который будет передаваться в запрос оплаты. | | `FREEKASSA_PAYMENT_IP` | Внешний IP вашего сервера, который будет передаваться в запрос оплаты. |
| `FREEKASSA_PAYMENT_METHOD_ID` | ID метода оплаты через магазин FreeKassa. По умолчанию `44`. | | `FREEKASSA_PAYMENT_METHOD_ID` | ID метода оплаты через магазин FreeKassa. По умолчанию `44`. |
| `STARS_ENABLED` | Включить/выключить Telegram Stars (`true`/`false`). | | `STARS_ENABLED` | Включить/выключить Telegram Stars (`true`/`false`). |
| `TRIBUTE_ENABLED`| Включить/выключить Tribute (`true`/`false`). |
| `PLATEGA_ENABLED`| Включить/выключить Platega (`true`/`false`). | | `PLATEGA_ENABLED`| Включить/выключить Platega (`true`/`false`). |
| `PLATEGA_MERCHANT_ID`| MerchantId из личного кабинета Platega. | | `PLATEGA_MERCHANT_ID`| MerchantId из личного кабинета Platega. |
| `PLATEGA_SECRET`| API секрет для запросов Platega. | | `PLATEGA_SECRET`| API секрет для запросов Platega. |
@@ -118,7 +117,6 @@
- `1_MONTH_ENABLED`: `true` или `false` - `1_MONTH_ENABLED`: `true` или `false`
- `RUB_PRICE_1_MONTH`: Цена в рублях - `RUB_PRICE_1_MONTH`: Цена в рублях
- `STARS_PRICE_1_MONTH`: Цена в Telegram Stars - `STARS_PRICE_1_MONTH`: Цена в Telegram Stars
- `TRIBUTE_LINK_1_MONTH`: Ссылка для оплаты через Tribute
Аналогичные переменные есть для `3_MONTHS`, `6_MONTHS`, `12_MONTHS`. Аналогичные переменные есть для `3_MONTHS`, `6_MONTHS`, `12_MONTHS`.
</details> </details>
@@ -155,7 +153,7 @@
Эта команда скачает образ и запустит сервис в фоновом режиме. Эта команда скачает образ и запустит сервис в фоновом режиме.
4. **Настройка вебхуков (Обязательно):** 4. **Настройка вебхуков (Обязательно):**
Вебхуки являются **обязательным** компонентом для работы бота, так как они используются для получения уведомлений от платежных систем (YooKassa, FreeKassa, CryptoPay, Tribute) и панели Remnawave. Вебхуки являются **обязательным** компонентом для работы бота, так как они используются для получения уведомлений от платежных систем (YooKassa, FreeKassa, CryptoPay, Platega, SeverPay) и панели Remnawave.
Вам понадобится обратный прокси (например, Nginx) для обработки HTTPS-трафика и перенаправления запросов на контейнер с ботом. Вам понадобится обратный прокси (например, Nginx) для обработки HTTPS-трафика и перенаправления запросов на контейнер с ботом.
@@ -165,7 +163,6 @@
- `https://<ваш_домен>/webhook/platega` → `http://remnawave-tg-shop:<WEB_SERVER_PORT>/webhook/platega` - `https://<ваш_домен>/webhook/platega` → `http://remnawave-tg-shop:<WEB_SERVER_PORT>/webhook/platega`
- `https://<ваш_домен>/webhook/severpay` → `http://remnawave-tg-shop:<WEB_SERVER_PORT>/webhook/severpay` - `https://<ваш_домен>/webhook/severpay` → `http://remnawave-tg-shop:<WEB_SERVER_PORT>/webhook/severpay`
- `https://<ваш_домен>/webhook/cryptopay` → `http://remnawave-tg-shop:<WEB_SERVER_PORT>/webhook/cryptopay` - `https://<ваш_домен>/webhook/cryptopay` → `http://remnawave-tg-shop:<WEB_SERVER_PORT>/webhook/cryptopay`
- `https://<ваш_домен>/webhook/tribute` → `http://remnawave-tg-shop:<WEB_SERVER_PORT>/webhook/tribute`
- `https://<ваш_домен>/webhook/panel` → `http://remnawave-tg-shop:<WEB_SERVER_PORT>/webhook/panel` - `https://<ваш_домен>/webhook/panel` → `http://remnawave-tg-shop:<WEB_SERVER_PORT>/webhook/panel`
- **Для Telegram:** Бот автоматически установит вебхук, если в `.env` указан `WEBHOOK_BASE_URL`. Путь будет `https://<ваш_домен>/<BOT_TOKEN>`. - **Для Telegram:** Бот автоматически установит вебхук, если в `.env` указан `WEBHOOK_BASE_URL`. Путь будет `https://<ваш_домен>/<BOT_TOKEN>`.
-11
View File
@@ -9,7 +9,6 @@ from bot.services.subscription_service import SubscriptionService
from bot.services.referral_service import ReferralService from bot.services.referral_service import ReferralService
from bot.services.promo_code_service import PromoCodeService from bot.services.promo_code_service import PromoCodeService
from bot.services.stars_service import StarsService from bot.services.stars_service import StarsService
from bot.services.tribute_service import TributeService
from bot.services.crypto_pay_service import CryptoPayService from bot.services.crypto_pay_service import CryptoPayService
from bot.services.panel_webhook_service import PanelWebhookService from bot.services.panel_webhook_service import PanelWebhookService
from bot.services.freekassa_service import FreeKassaService from bot.services.freekassa_service import FreeKassaService
@@ -47,15 +46,6 @@ def build_core_services(
subscription_service=subscription_service, subscription_service=subscription_service,
referral_service=referral_service, referral_service=referral_service,
) )
tribute_service = TributeService(
bot,
settings,
i18n,
async_session_factory,
panel_service,
subscription_service,
referral_service,
)
platega_service = PlategaService( platega_service = PlategaService(
bot=bot, bot=bot,
settings=settings, settings=settings,
@@ -100,7 +90,6 @@ def build_core_services(
"stars_service": stars_service, "stars_service": stars_service,
"cryptopay_service": cryptopay_service, "cryptopay_service": cryptopay_service,
"freekassa_service": freekassa_service, "freekassa_service": freekassa_service,
"tribute_service": tribute_service,
"panel_webhook_service": panel_webhook_service, "panel_webhook_service": panel_webhook_service,
"yookassa_service": yookassa_service, "yookassa_service": yookassa_service,
"platega_service": platega_service, "platega_service": platega_service,
-7
View File
@@ -29,7 +29,6 @@ async def build_and_start_web_app(
"stars_service", "stars_service",
"freekassa_service", "freekassa_service",
"cryptopay_service", "cryptopay_service",
"tribute_service",
"panel_webhook_service", "panel_webhook_service",
"platega_service", "platega_service",
"severpay_service", "severpay_service",
@@ -50,18 +49,12 @@ async def build_and_start_web_app(
) )
from bot.handlers.user.payment import yookassa_webhook_route from bot.handlers.user.payment import yookassa_webhook_route
from bot.services.tribute_service import tribute_webhook_route
from bot.services.crypto_pay_service import cryptopay_webhook_route from bot.services.crypto_pay_service import cryptopay_webhook_route
from bot.services.panel_webhook_service import panel_webhook_route from bot.services.panel_webhook_service import panel_webhook_route
from bot.services.freekassa_service import freekassa_webhook_route from bot.services.freekassa_service import freekassa_webhook_route
from bot.services.platega_service import platega_webhook_route from bot.services.platega_service import platega_webhook_route
from bot.services.severpay_service import severpay_webhook_route from bot.services.severpay_service import severpay_webhook_route
tribute_path = settings.tribute_webhook_path
if tribute_path.startswith("/"):
app.router.add_post(tribute_path, tribute_webhook_route)
logging.info(f"Tribute webhook route configured at: [POST] {tribute_path}")
cp_path = settings.cryptopay_webhook_path cp_path = settings.cryptopay_webhook_path
if cp_path.startswith("/"): if cp_path.startswith("/"):
app.router.add_post(cp_path, cryptopay_webhook_route) app.router.add_post(cp_path, cryptopay_webhook_route)
-1
View File
@@ -60,7 +60,6 @@ def format_payment_text(payment: Payment, i18n: JsonI18n, lang: str) -> str:
provider_text = { provider_text = {
'yookassa': 'YooKassa', 'yookassa': 'YooKassa',
'tribute': 'Tribute',
'telegram_stars': 'Telegram Stars', 'telegram_stars': 'Telegram Stars',
'cryptopay': 'CryptoPay', 'cryptopay': 'CryptoPay',
'freekassa': 'FreeKassa', 'freekassa': 'FreeKassa',
+12 -23
View File
@@ -125,17 +125,6 @@ async def my_subscription_command_handler(
end_date = active.get("end_date") end_date = active.get("end_date")
days_left = (end_date.date() - datetime.now().date()).days if end_date else 0 days_left = (end_date.date() - datetime.now().date()).days if end_date else 0
tribute_hint = ""
if active.get("status_from_panel", "").lower() == "active":
local_sub = await subscription_dal.get_active_subscription_by_user_id(session, event.from_user.id)
if local_sub:
if local_sub.provider == "tribute":
link = None
link = settings.tribute_payment_links.get(local_sub.duration_months or 1) if hasattr(settings, "tribute_payment_links") else None
tribute_hint = "\n\n" + (
get_text("subscription_tribute_notice_with_link", link=link) if link else get_text("subscription_tribute_notice")
)
text = get_text( text = get_text(
"my_subscription_details", "my_subscription_details",
end_date=end_date.strftime("%Y-%m-%d") if end_date else "N/A", end_date=end_date.strftime("%Y-%m-%d") if end_date else "N/A",
@@ -224,8 +213,8 @@ async def my_subscription_command_handler(
) )
]) ])
# 2) Auto-renew toggle (if supported and not tribute) # 2) Auto-renew toggle (YooKassa only)
if local_sub and local_sub.provider != "tribute" and settings.yookassa_autopayments_active: if local_sub and local_sub.provider == "yookassa" and settings.yookassa_autopayments_active:
toggle_text = ( toggle_text = (
get_text("autorenew_disable_button") if local_sub.auto_renew_enabled else get_text("autorenew_enable_button") get_text("autorenew_disable_button") if local_sub.auto_renew_enabled else get_text("autorenew_enable_button")
) )
@@ -254,17 +243,17 @@ async def my_subscription_command_handler(
except Exception: except Exception:
pass pass
try: try:
await event.message.edit_text(text + tribute_hint, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True) await event.message.edit_text(text, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True)
except Exception: except Exception:
await bot.send_message( await bot.send_message(
chat_id=target.chat.id, chat_id=target.chat.id,
text=text + tribute_hint, text=text,
reply_markup=markup, reply_markup=markup,
parse_mode="HTML", parse_mode="HTML",
disable_web_page_preview=True, disable_web_page_preview=True,
) )
else: else:
await target.answer(text + tribute_hint, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True) await target.answer(text, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True)
@router.callback_query(F.data == "main_action:my_devices") @router.callback_query(F.data == "main_action:my_devices")
@@ -452,8 +441,8 @@ async def toggle_autorenew_handler(
if not sub or sub.user_id != callback.from_user.id: if not sub or sub.user_id != callback.from_user.id:
await callback.answer(get_text("error_try_again"), show_alert=True) await callback.answer(get_text("error_try_again"), show_alert=True)
return return
if sub.provider == "tribute": if sub.provider != "yookassa":
await callback.answer(get_text("subscription_autorenew_not_supported_for_tribute"), show_alert=True) await callback.answer(get_text("error_try_again"), show_alert=True)
return return
if enable: if enable:
has_saved_card = await user_billing_dal.user_has_saved_payment_method(session, callback.from_user.id) has_saved_card = await user_billing_dal.user_has_saved_payment_method(session, callback.from_user.id)
@@ -510,8 +499,8 @@ async def confirm_autorenew_handler(
if not sub or sub.user_id != callback.from_user.id: if not sub or sub.user_id != callback.from_user.id:
await callback.answer(get_text("error_try_again"), show_alert=True) await callback.answer(get_text("error_try_again"), show_alert=True)
return return
if sub.provider == "tribute": if sub.provider != "yookassa":
await callback.answer(get_text("subscription_autorenew_not_supported_for_tribute"), show_alert=True) await callback.answer(get_text("error_try_again"), show_alert=True)
return return
if enable: if enable:
has_saved_card = await user_billing_dal.user_has_saved_payment_method(session, callback.from_user.id) has_saved_card = await user_billing_dal.user_has_saved_payment_method(session, callback.from_user.id)
@@ -549,7 +538,7 @@ async def autorenew_cancel_from_webhook_button(
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
# Disable auto-renew on the active subscription (non-tribute) # Disable auto-renew on the active subscription
from db.dal import subscription_dal from db.dal import subscription_dal
sub = await subscription_dal.get_active_subscription_by_user_id(session, callback.from_user.id) sub = await subscription_dal.get_active_subscription_by_user_id(session, callback.from_user.id)
if not sub: if not sub:
@@ -558,9 +547,9 @@ async def autorenew_cancel_from_webhook_button(
except Exception: except Exception:
pass pass
return return
if sub.provider == "tribute": if sub.provider != "yookassa":
try: try:
await callback.answer(get_text("subscription_autorenew_not_supported_for_tribute"), show_alert=True) await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception: except Exception:
pass pass
return return
@@ -52,12 +52,10 @@ async def select_subscription_period_callback_handler(
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
text_content = get_text("choose_payment_method") text_content = get_text("choose_payment_method")
tribute_url = settings.tribute_payment_links.get(months)
stars_price = settings.stars_subscription_options.get(months) stars_price = settings.stars_subscription_options.get(months)
reply_markup = get_payment_method_keyboard( reply_markup = get_payment_method_keyboard(
months, months,
price_rub, price_rub,
tribute_url,
stars_price, stars_price,
currency_symbol_val, currency_symbol_val,
current_lang, current_lang,
-3
View File
@@ -112,7 +112,6 @@ def get_subscription_options_keyboard(subscription_options: Dict[
def get_payment_method_keyboard(months: int, price: float, def get_payment_method_keyboard(months: int, price: float,
tribute_url: Optional[str],
stars_price: Optional[int], stars_price: Optional[int],
currency_symbol_val: str, lang: str, currency_symbol_val: str, lang: str,
i18n_instance, settings: Settings) -> InlineKeyboardMarkup: i18n_instance, settings: Settings) -> InlineKeyboardMarkup:
@@ -139,8 +138,6 @@ def get_payment_method_keyboard(months: int, price: float,
text=_("pay_with_yookassa_button"), text=_("pay_with_yookassa_button"),
callback_data=f"pay_yk:{months}:{price}", callback_data=f"pay_yk:{months}:{price}",
) )
elif method == "tribute" and settings.TRIBUTE_ENABLED and tribute_url:
builder.button(text=_("pay_with_tribute_button"), url=tribute_url)
elif method == "stars" and settings.STARS_ENABLED and stars_price is not None: elif method == "stars" and settings.STARS_ENABLED and stars_price is not None:
builder.button( builder.button(
text=_("pay_with_stars_button"), text=_("pay_with_stars_button"),
-2
View File
@@ -33,7 +33,6 @@ from bot.services.subscription_service import SubscriptionService
from bot.services.referral_service import ReferralService from bot.services.referral_service import ReferralService
from bot.services.promo_code_service import PromoCodeService from bot.services.promo_code_service import PromoCodeService
from bot.services.stars_service import StarsService from bot.services.stars_service import StarsService
from bot.services.tribute_service import TributeService, tribute_webhook_route
from bot.services.crypto_pay_service import CryptoPayService, cryptopay_webhook_route from bot.services.crypto_pay_service import CryptoPayService, cryptopay_webhook_route
from bot.handlers.user import payment as user_payment_webhook_module from bot.handlers.user import payment as user_payment_webhook_module
@@ -200,7 +199,6 @@ async def on_shutdown_configured(dispatcher: Dispatcher):
"panel_service", "panel_service",
"cryptopay_service", "cryptopay_service",
"freekassa_service", "freekassa_service",
"tribute_service",
"panel_webhook_service", "panel_webhook_service",
"yookassa_service", "yookassa_service",
"promo_code_service", "promo_code_service",
-1
View File
@@ -240,7 +240,6 @@ class NotificationService:
"freekassa": "💳", "freekassa": "💳",
"cryptopay": "", "cryptopay": "",
"stars": "", "stars": "",
"tribute": "💎",
"platega": "💳", "platega": "💳",
"severpay": "💳", "severpay": "💳",
}.get(payment_provider.lower(), "💰") }.get(payment_provider.lower(), "💰")
+4 -131
View File
@@ -12,7 +12,6 @@ from .panel_api_service import PanelApiService
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
from bot.keyboards.inline.user_keyboards import get_subscribe_only_markup, get_autorenew_cancel_keyboard from bot.keyboards.inline.user_keyboards import get_subscribe_only_markup, get_autorenew_cancel_keyboard
from db.dal import user_dal from db.dal import user_dal
from bot.utils.date_utils import add_months
EVENT_MAP = { EVENT_MAP = {
"user.expires_in_72_hours": (3, "subscription_72h_notification"), "user.expires_in_72_hours": (3, "subscription_72h_notification"),
@@ -44,128 +43,6 @@ class PanelWebhookService:
except Exception as e: except Exception as e:
logging.error(f"Failed to send notification to {user_id}: {e}") logging.error(f"Failed to send notification to {user_id}: {e}")
async def _handle_expired_subscription(self, session, user_id: int, user_payload: dict,
lang: str, markup, first_name: str) -> bool:
"""Handle expired subscription - auto-renew tribute users if no cancellation was received.
Returns True if an auto-renewal was performed (and renewal message sent), False otherwise.
"""
from db.dal import subscription_dal, payment_dal
from datetime import datetime, timezone
try:
auto_renewed = False
# Check if user has tribute subscriptions that weren't cancelled
user_subs = await subscription_dal.get_active_subscriptions_for_user(session, user_id)
for sub in user_subs:
# Check if this subscription was marked as cancelled (from tribute cancellation webhook)
if sub.status_from_panel == 'CANCELLED':
logging.info(f"Subscription {sub.subscription_id} for user {user_id} was cancelled, skipping auto-renewal")
continue
# Check if this user has tribute payments
last_tribute_duration = await payment_dal.get_last_tribute_payment_duration(session, user_id)
if last_tribute_duration is not None:
# This user has tribute payments, auto-renew for the same duration
logging.info(f"Auto-renewing tribute subscription for user {user_id} for {last_tribute_duration} months")
# Extend subscription by the last payment duration (calendar months)
new_end_date = add_months(datetime.now(timezone.utc), last_tribute_duration)
# Update local DB subscription
await subscription_dal.update_subscription(
session,
sub.subscription_id,
{
'end_date': new_end_date,
'status_from_panel': 'ACTIVE',
'is_active': True
}
)
# Update panel expiry to ensure actual service access is extended
try:
panel_payload = {
"uuid": sub.panel_user_uuid,
"expireAt": new_end_date.isoformat(timespec='milliseconds').replace('+00:00', 'Z'),
"status": "ACTIVE",
}
panel_update_resp = await self.panel_service.update_user_details_on_panel(
sub.panel_user_uuid,
panel_payload,
log_response=True,
)
if panel_update_resp:
logging.info(
f"Panel expiry updated for user {user_id} (panel_uuid {sub.panel_user_uuid}) to {new_end_date}"
)
except Exception as e_panel:
logging.error(
f"Failed to update panel expiry for user {user_id} (panel_uuid {sub.panel_user_uuid}): {e_panel}")
# Create a succeeded payment record in DB with the same amount/currency as last tribute payment
try:
last_payment = await payment_dal.get_last_tribute_payment(session, user_id)
if last_payment and last_payment.amount and last_payment.currency:
provider_payment_id = (
f"tribute_auto_{user_id}_{sub.subscription_id}_"
f"{new_end_date.strftime('%Y%m%d')}"
)
created_payment = await payment_dal.ensure_payment_with_provider_id(
session,
user_id=user_id,
amount=float(last_payment.amount),
currency=last_payment.currency,
months=last_tribute_duration,
description="Auto-renewal (panel webhook)",
provider="tribute",
provider_payment_id=provider_payment_id,
)
if created_payment:
logging.info(
f"Auto-renew payment recorded (id={created_payment.payment_id}) for user {user_id} amount={created_payment.amount} {created_payment.currency} months={last_tribute_duration}"
)
else:
logging.warning(
f"Could not create auto-renew payment for user {user_id}: previous tribute payment not found or missing amount/currency")
except Exception as e_pay:
logging.error(
f"Failed to create auto-renew payment record for user {user_id}: {e_pay}",
exc_info=True,
)
# Send auto-renewal notification
_ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k
auto_renewal_msg = _(
"tribute_auto_renewal",
default="🔄 <b>Подписка автоматически продлена</b>\n\n"
"Ваша подписка Tribute была автоматически продлена на {months} мес.\n"
"Новая дата окончания: {end_date}",
user_name=first_name,
months=last_tribute_duration,
end_date=new_end_date.strftime('%Y-%m-%d')
)
try:
await self.bot.send_message(
user_id,
auto_renewal_msg,
reply_markup=markup,
parse_mode="HTML"
)
auto_renewed = True
except Exception as e:
logging.error(f"Failed to send auto-renewal notification to user {user_id}: {e}")
await session.commit()
return auto_renewed
except Exception as e:
logging.error(f"Error handling expired subscription for user {user_id}: {e}")
await session.rollback()
return False
async def handle_event(self, event_name: str, user_payload: dict): async def handle_event(self, event_name: str, user_payload: dict):
telegram_id = user_payload.get("telegramId") telegram_id = user_payload.get("telegramId")
if not telegram_id: if not telegram_id:
@@ -193,7 +70,7 @@ class PanelWebhookService:
async with self.async_session_factory() as session: async with self.async_session_factory() as session:
from db.dal import subscription_dal from db.dal import subscription_dal
sub = await subscription_dal.get_active_subscription_by_user_id(session, user_id) sub = await subscription_dal.get_active_subscription_by_user_id(session, user_id)
if sub and sub.auto_renew_enabled and sub.provider != 'tribute': if sub and sub.auto_renew_enabled and sub.provider == 'yookassa':
try: try:
ok = await subscription_service.charge_subscription_renewal(session, sub) ok = await subscription_service.charge_subscription_renewal(session, sub)
# If initiation succeeded, suppress the 24h reminder by returning early # If initiation succeeded, suppress the 24h reminder by returning early
@@ -208,7 +85,7 @@ class PanelWebhookService:
except Exception: except Exception:
logging.exception("Auto-renew trigger (24h) failed pre-check") logging.exception("Auto-renew trigger (24h) failed pre-check")
if days_left <= self.settings.SUBSCRIPTION_NOTIFY_DAYS_BEFORE: if days_left <= self.settings.SUBSCRIPTION_NOTIFY_DAYS_BEFORE:
# For 48h event, if auto-renew is enabled and not tribute, show special notice with cancel button # For 48h event, if auto-renew is enabled, show special notice with cancel button
if days_left == 2: if days_left == 2:
async with self.async_session_factory() as session: async with self.async_session_factory() as session:
from db.dal import subscription_dal from db.dal import subscription_dal
@@ -220,7 +97,7 @@ class PanelWebhookService:
getattr(sub, 'auto_renew_enabled', None) if sub else None, getattr(sub, 'auto_renew_enabled', None) if sub else None,
getattr(sub, 'provider', None) if sub else None, getattr(sub, 'provider', None) if sub else None,
) )
if sub and sub.auto_renew_enabled and sub.provider != 'tribute': if sub and sub.auto_renew_enabled and sub.provider == 'yookassa':
cancel_kb = get_autorenew_cancel_keyboard(lang, self.i18n) cancel_kb = get_autorenew_cancel_keyboard(lang, self.i18n)
await self._send_message( await self._send_message(
user_id, user_id,
@@ -239,11 +116,7 @@ class PanelWebhookService:
end_date=user_payload.get("expireAt", "")[:10], end_date=user_payload.get("expireAt", "")[:10],
) )
elif event_name == "user.expired": elif event_name == "user.expired":
# Check if this is a tribute user that should be auto-renewed (regardless of notification settings) if self.settings.SUBSCRIPTION_NOTIFY_ON_EXPIRE:
auto_renewed = await self._handle_expired_subscription(session, user_id, user_payload, lang, markup, first_name)
# If auto-renewed via Tribute, suppress expiration notification. Otherwise, send it if enabled.
if not auto_renewed and self.settings.SUBSCRIPTION_NOTIFY_ON_EXPIRE:
await self._send_message( await self._send_message(
user_id, user_id,
lang, lang,
+3 -3
View File
@@ -517,7 +517,7 @@ class SubscriptionService:
"status_from_panel": "ACTIVE", "status_from_panel": "ACTIVE",
"traffic_limit_bytes": self.settings.user_traffic_limit_bytes, "traffic_limit_bytes": self.settings.user_traffic_limit_bytes,
"provider": provider, "provider": provider,
"skip_notifications": provider == "tribute" and self.settings.TRIBUTE_SKIP_NOTIFICATIONS, "skip_notifications": False,
"auto_renew_enabled": auto_renew_should_enable, "auto_renew_enabled": auto_renew_should_enable,
} }
try: try:
@@ -828,8 +828,8 @@ class SubscriptionService:
# If autopayments are disabled globally, skip charging attempts # If autopayments are disabled globally, skip charging attempts
if not self.settings.yookassa_autopayments_active: if not self.settings.yookassa_autopayments_active:
return True return True
if sub.provider == "tribute": if sub.provider != "yookassa":
# Tribute is paid externally; we do not auto-charge here logging.info("Auto-renew skipped: provider %s does not support auto-renew", sub.provider)
return True return True
from db.dal.user_billing_dal import get_user_default_payment_method from db.dal.user_billing_dal import get_user_default_payment_method
-335
View File
@@ -1,335 +0,0 @@
import logging
import hmac
import hashlib
import json
from typing import Optional
from aiohttp import web
from aiogram import Bot
from sqlalchemy.orm import sessionmaker
from config.settings import Settings
from bot.middlewares.i18n import JsonI18n
from bot.services.subscription_service import SubscriptionService
from bot.services.panel_api_service import PanelApiService
from bot.services.referral_service import ReferralService
from .notification_service import NotificationService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
from db.dal import payment_dal, user_dal, subscription_dal
from bot.utils.text_sanitizer import sanitize_display_name, username_for_display
def convert_period_to_months(period: Optional[str]) -> int:
"""Map Tribute subscription period strings to months."""
if not period:
return 1
mapping = {
"monthly": 1,
"quarterly": 3,
"3-month": 3,
"3months": 3,
"3-months": 3,
"q": 3,
"halfyearly": 6,
"yearly": 12,
"annual": 12,
"y": 12,
}
return mapping.get(period.lower(), 1)
class TributeService:
def __init__(
self,
bot: Bot,
settings: Settings,
i18n: JsonI18n,
async_session_factory: sessionmaker,
panel_service: PanelApiService,
subscription_service: SubscriptionService,
referral_service: ReferralService,
):
self.bot = bot
self.settings = settings
self.i18n = i18n
self.async_session_factory = async_session_factory
self.panel_service = panel_service
self.subscription_service = subscription_service
self.referral_service = referral_service
async def handle_webhook(self, raw_body: bytes, signature_header: Optional[str]) -> web.Response:
settings = self.settings
bot = self.bot
i18n = self.i18n
async_session_factory = self.async_session_factory
subscription_service = self.subscription_service
referral_service = self.referral_service
def ok(data: Optional[dict] = None) -> web.Response:
payload = {"status": "ok"}
if data:
payload.update(data)
return web.json_response(payload, status=200)
def ignored(reason: str) -> web.Response:
return web.json_response({"status": "ignored", "reason": reason}, status=200)
def bad_request(reason: str) -> web.Response:
return web.json_response({"status": "error", "reason": reason}, status=400)
if settings.TRIBUTE_API_KEY:
if not signature_header:
return web.json_response({"status": "error", "reason": "no_signature"}, status=403)
expected_sig = hmac.new(settings.TRIBUTE_API_KEY.encode(), raw_body,
hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected_sig, signature_header):
return web.json_response({"status": "error", "reason": "invalid_signature"}, status=403)
try:
payload = json.loads(raw_body.decode())
except Exception:
return bad_request("invalid_json")
logging.info(
"Tribute webhook data: %s",
json.dumps(payload, ensure_ascii=False),
)
# Tribute webhook spec: only two events are sent
# name: new_subscription | cancelled_subscription
event_name = payload.get("name")
data = payload.get("payload", {})
# Mandatory routing fields
user_id = data.get("telegram_user_id")
if not user_id:
# Permanent format issue — acknowledge to avoid retries
return ignored("missing_telegram_user_id")
period_val = data.get("period")
months = convert_period_to_months(period_val)
# Tribute sends amount in minor units (kopecks/cents). Convert to major units before persisting.
amount_value = data.get("amount") or data.get("price")
currency = (data.get("currency") or settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
if amount_value is not None:
try:
amount_minor_units = float(amount_value)
except (TypeError, ValueError):
amount_minor_units = 0.0
amount_float = round(amount_minor_units / 100.0, 2)
else:
amount_float = 0.0
async with async_session_factory() as session:
if event_name == "new_subscription":
# Use a unique, idempotent provider payment id per webhook event
# Prefer explicit event/payment identifiers if present; otherwise fall back to payload hash suffix
candidate_event_id = (
str(data.get("event_id") or data.get("payment_id") or data.get("purchase_id") or data.get("invoice_id") or "")
)
if candidate_event_id:
provider_payment_id = candidate_event_id
else:
# Combine subscription_id (if any) with a stable hash of the raw payload to ensure uniqueness per event
sub_id_part = str(data.get("subscription_id") or "sub")
payload_hash = hashlib.sha256(raw_body).hexdigest()[:16]
provider_payment_id = f"{sub_id_part}:{payload_hash}"
# Idempotent ensure payment
payment_record = await payment_dal.ensure_payment_with_provider_id(
session,
user_id=int(user_id),
amount=amount_float,
currency=currency,
months=months,
description="Tribute subscription",
provider="tribute",
provider_payment_id=provider_payment_id,
)
activation_details = await subscription_service.activate_subscription(
session,
int(user_id),
months,
float(amount_float),
payment_record.payment_id,
provider="tribute",
)
referral_bonus = await referral_service.apply_referral_bonuses_for_payment(
session,
int(user_id),
months,
current_payment_db_id=payment_record.payment_id,
skip_if_active_before_payment=False,
)
await session.commit()
db_user = await user_dal.get_user_by_id(session, int(user_id))
lang = db_user.language_code if db_user and db_user.language_code else settings.DEFAULT_LANGUAGE
_ = lambda k, **kw: i18n.gettext(lang, k, **kw)
applied_ref_days = referral_bonus.get('referee_bonus_applied_days') if referral_bonus else None
final_end = (referral_bonus.get('referee_new_end_date')
if referral_bonus else None)
if not final_end:
final_end = activation_details.get('end_date')
if final_end:
config_link = activation_details.get("subscription_url") or _(
"config_link_not_available"
)
if applied_ref_days:
inviter_name_display = _('friend_placeholder')
if db_user and db_user.referred_by_id:
inviter = await user_dal.get_user_by_id(session, db_user.referred_by_id)
if inviter:
safe_name = sanitize_display_name(inviter.first_name) if inviter.first_name else None
if safe_name:
inviter_name_display = safe_name
elif inviter.username:
inviter_name_display = username_for_display(inviter.username, with_at=False)
success_msg = _(
"payment_successful_with_referral_bonus_full",
months=months,
base_end_date=activation_details["end_date"].strftime('%Y-%m-%d'),
bonus_days=applied_ref_days,
final_end_date=final_end.strftime('%Y-%m-%d'),
inviter_name=inviter_name_display,
config_link=config_link,
)
else:
success_msg = _(
"payment_successful_full",
months=months,
end_date=final_end.strftime('%Y-%m-%d'),
config_link=config_link,
)
markup = get_connect_and_main_keyboard(
lang,
i18n,
settings,
config_link,
preserve_message=True,
)
try:
# Use user's DB language in success messages prepared above
await bot.send_message(
int(user_id),
success_msg,
reply_markup=markup,
parse_mode="HTML",
disable_web_page_preview=True,
)
except Exception as e:
logging.error(
f"Failed to send Tribute payment success message to user {user_id}: {e}")
# Send notification about payment
try:
notification_service = NotificationService(bot, settings, i18n)
user = await user_dal.get_user_by_id(session, int(user_id))
await notification_service.notify_payment_received(
user_id=int(user_id),
amount=float(amount_float),
currency=currency,
months=months,
payment_provider="tribute",
username=user.username if user else None
)
except Exception as e:
logging.error(f"Failed to send tribute payment notification: {e}")
elif event_name == "cancelled_subscription":
await self._handle_tribute_cancellation(session, int(user_id), bot, i18n)
else:
await session.commit()
# Acknowledge to Tribute that webhook was received and processed/accepted
return ok({"event": event_name or "unknown"})
async def _handle_tribute_cancellation(self, session, user_id: int, bot: Bot, i18n: JsonI18n):
"""Handle tribute subscription cancellation - set subscription to 1 day grace period"""
from datetime import datetime, timezone, timedelta
from db.dal import subscription_dal, user_dal
from bot.keyboards.inline.user_keyboards import get_subscribe_only_markup
try:
grace_days = 1
grace_end = datetime.now(timezone.utc) + timedelta(days=grace_days)
active_subscriptions = await subscription_dal.get_active_subscriptions_for_user(session, user_id)
panel_users_updated: set[str] = set()
for sub in active_subscriptions:
updated_sub = await subscription_dal.update_subscription(
session,
sub.subscription_id,
{
"end_date": grace_end,
"status_from_panel": "CANCELLED",
"skip_notifications": True,
},
)
panel_uuid = updated_sub.panel_user_uuid if updated_sub else None
if panel_uuid and panel_uuid not in panel_users_updated:
panel_users_updated.add(panel_uuid)
panel_payload = {
"expireAt": grace_end.isoformat(timespec="milliseconds").replace("+00:00", "Z"),
}
try:
await self.panel_service.update_user_details_on_panel(
panel_uuid,
panel_payload,
log_response=False,
)
except Exception as panel_err:
logging.error(
f"Failed to update panel expiry for user {user_id} (panel_uuid {panel_uuid}) during Tribute cancellation: {panel_err}")
await session.commit()
# Send notification about cancellation if enabled
if not self.settings.TRIBUTE_SKIP_CANCELLATION_NOTIFICATIONS:
db_user = await user_dal.get_user_by_id(session, user_id)
lang = db_user.language_code if db_user and db_user.language_code else self.settings.DEFAULT_LANGUAGE
first_name = db_user.first_name or f"User {user_id}" if db_user else f"User {user_id}"
_ = lambda k, **kw: i18n.gettext(lang, k, **kw) if i18n else k
markup = get_subscribe_only_markup(lang, i18n)
cancellation_msg = _(
"tribute_subscription_cancelled",
default="🚨 <b>Подписка отменена</b>\n\n"
"Ваша подписка Tribute была отменена. У вас есть 24 часа для восстановления доступа, "
"после чего подписка будет заблокирована.\n\n"
"Для продления подписки нажмите кнопку ниже.",
user_name=first_name
)
try:
await bot.send_message(
int(user_id),
cancellation_msg,
reply_markup=markup,
parse_mode="HTML"
)
except Exception as e:
logging.error(f"Failed to send tribute cancellation notification to user {user_id}: {e}")
logging.info(f"Tribute subscription cancelled for user {user_id}, grace period set to 1 day")
except Exception as e:
logging.error(f"Error handling tribute cancellation for user {user_id}: {e}")
await session.rollback()
async def tribute_webhook_route(request: web.Request):
"""AIOHTTP route handler for Tribute webhook calls."""
tribute_service: TributeService = request.app['tribute_service']
raw_body = await request.read()
signature_header = request.headers.get('trbt-signature')
return await tribute_service.handle_webhook(raw_body, signature_header)
+1 -43
View File
@@ -84,10 +84,9 @@ class Settings(BaseSettings):
YOOKASSA_ENABLED: bool = Field(default=True) YOOKASSA_ENABLED: bool = Field(default=True)
STARS_ENABLED: bool = Field(default=True) STARS_ENABLED: bool = Field(default=True)
TRIBUTE_ENABLED: bool = Field(default=True)
PAYMENT_METHODS_ORDER: Optional[str] = Field( PAYMENT_METHODS_ORDER: Optional[str] = Field(
default=None, default=None,
description="Comma-separated list of payment methods to show (e.g., severpay,freekassa,yookassa,platega,stars,cryptopay,tribute)", description="Comma-separated list of payment methods to show (e.g., severpay,freekassa,yookassa,platega,stars,cryptopay)",
) )
MONTH_1_ENABLED: bool = Field(default=True, alias="1_MONTH_ENABLED") MONTH_1_ENABLED: bool = Field(default=True, alias="1_MONTH_ENABLED")
@@ -104,15 +103,6 @@ class Settings(BaseSettings):
STARS_PRICE_3_MONTHS: Optional[int] = Field(default=None) STARS_PRICE_3_MONTHS: Optional[int] = Field(default=None)
STARS_PRICE_6_MONTHS: Optional[int] = Field(default=None) STARS_PRICE_6_MONTHS: Optional[int] = Field(default=None)
STARS_PRICE_12_MONTHS: Optional[int] = Field(default=None) STARS_PRICE_12_MONTHS: Optional[int] = Field(default=None)
TRIBUTE_LINK_1_MONTH: Optional[str] = Field(default=None)
TRIBUTE_LINK_3_MONTHS: Optional[str] = Field(default=None)
TRIBUTE_LINK_6_MONTHS: Optional[str] = Field(default=None)
TRIBUTE_LINK_12_MONTHS: Optional[str] = Field(default=None)
TRIBUTE_API_KEY: Optional[str] = Field(default=None)
TRIBUTE_SKIP_NOTIFICATIONS: bool = Field(default=True, description="Skip renewal notifications for Tribute payments")
TRIBUTE_SKIP_CANCELLATION_NOTIFICATIONS: bool = Field(default=False, description="Skip cancellation notifications for Tribute payments")
PANEL_WEBHOOK_SECRET: Optional[str] = Field(default=None) PANEL_WEBHOOK_SECRET: Optional[str] = Field(default=None)
SUBSCRIPTION_NOTIFICATIONS_ENABLED: bool = Field(default=True) SUBSCRIPTION_NOTIFICATIONS_ENABLED: bool = Field(default=True)
@@ -265,21 +255,6 @@ class Settings(BaseSettings):
return f"{base.rstrip('/')}{self.yookassa_webhook_path}" return f"{base.rstrip('/')}{self.yookassa_webhook_path}"
return None return None
@computed_field
@property
def tribute_webhook_path(self) -> str:
return "/webhook/tribute"
@computed_field
@property
def tribute_full_webhook_url(self) -> Optional[str]:
base = self.WEBHOOK_BASE_URL
if base:
return f"{base.rstrip('/')}{self.tribute_webhook_path}"
return None
@computed_field
@property
def panel_webhook_path(self) -> str: def panel_webhook_path(self) -> str:
return "/webhook/panel" return "/webhook/panel"
@@ -385,22 +360,6 @@ class Settings(BaseSettings):
options[12] = self.STARS_PRICE_12_MONTHS options[12] = self.STARS_PRICE_12_MONTHS
return options return options
@computed_field
@property
def tribute_payment_links(self) -> Dict[int, str]:
links: Dict[int, str] = {}
if self.TRIBUTE_ENABLED and self.MONTH_1_ENABLED and self.TRIBUTE_LINK_1_MONTH:
links[1] = self.TRIBUTE_LINK_1_MONTH
if self.TRIBUTE_ENABLED and self.MONTH_3_ENABLED and self.TRIBUTE_LINK_3_MONTHS:
links[3] = self.TRIBUTE_LINK_3_MONTHS
if self.TRIBUTE_ENABLED and self.MONTH_6_ENABLED and self.TRIBUTE_LINK_6_MONTHS:
links[6] = self.TRIBUTE_LINK_6_MONTHS
if self.TRIBUTE_ENABLED and self.MONTH_12_ENABLED and self.TRIBUTE_LINK_12_MONTHS:
links[12] = self.TRIBUTE_LINK_12_MONTHS
return links
@computed_field
@property
def referral_bonus_inviter(self) -> Dict[int, int]: def referral_bonus_inviter(self) -> Dict[int, int]:
bonuses: Dict[int, int] = {} bonuses: Dict[int, int] = {}
if self.REFERRAL_BONUS_DAYS_INVITER_1_MONTH is not None: if self.REFERRAL_BONUS_DAYS_INVITER_1_MONTH is not None:
@@ -444,7 +403,6 @@ class Settings(BaseSettings):
"platega", "platega",
"severpay", "severpay",
"yookassa", "yookassa",
"tribute",
"stars", "stars",
"cryptopay", "cryptopay",
] ]
-25
View File
@@ -239,31 +239,6 @@ async def get_financial_statistics(session: AsyncSession) -> Dict[str, Any]:
} }
async def get_last_tribute_payment_duration(session: AsyncSession, user_id: int) -> Optional[int]:
"""Get duration in months from the last successful tribute payment for a user."""
stmt = select(Payment.subscription_duration_months).where(
and_(
Payment.user_id == user_id,
Payment.provider == 'tribute',
Payment.status == 'succeeded'
)
).order_by(Payment.created_at.desc()).limit(1)
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def get_last_tribute_payment(
session: AsyncSession, user_id: int) -> Optional[Payment]:
"""Return the most recent succeeded Tribute payment for the user."""
stmt = (select(Payment).where(
and_(Payment.user_id == user_id, Payment.provider == 'tribute',
Payment.status == 'succeeded')).order_by(
Payment.created_at.desc()).limit(1))
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def get_user_total_paid(session: AsyncSession, user_id: int) -> float: async def get_user_total_paid(session: AsyncSession, user_id: int) -> float:
"""Get total amount paid by a specific user (sum of all succeeded payments).""" """Get total amount paid by a specific user (sum of all succeeded payments)."""
stmt = select(func.sum(Payment.amount)).where( stmt = select(func.sum(Payment.amount)).where(
-6
View File
@@ -41,7 +41,6 @@
"pay_with_severpay_button": "💳 SeverPay", "pay_with_severpay_button": "💳 SeverPay",
"back_to_payment_methods_button": "⬅️ Back", "back_to_payment_methods_button": "⬅️ Back",
"pay_with_cryptopay_button": "💎 CryptoBot", "pay_with_cryptopay_button": "💎 CryptoBot",
"pay_with_tribute_button": "❤️ Tribute",
"pay_with_stars_button": "🌟 Telegram Stars", "pay_with_stars_button": "🌟 Telegram Stars",
"connect_button": "🔗 Connect", "connect_button": "🔗 Connect",
"cancel_button": "❌ Cancel", "cancel_button": "❌ Cancel",
@@ -240,8 +239,6 @@
"autorenew_48h_charge_tomorrow_notice": "🔔 Reminder\n\nTomorrow an automatic charge will occur to renew your subscription. If you don't want auto-renew, disable it using the button below.", "autorenew_48h_charge_tomorrow_notice": "🔔 Reminder\n\nTomorrow an automatic charge will occur to renew your subscription. If you don't want auto-renew, disable it using the button below.",
"autorenew_confirm_enable": "🔄 Enable auto-renew? An automatic charge will be attempted before your subscription ends.", "autorenew_confirm_enable": "🔄 Enable auto-renew? An automatic charge will be attempted before your subscription ends.",
"autorenew_confirm_disable": "🛑 Disable auto-renew? No further automatic charges will occur.", "autorenew_confirm_disable": "🛑 Disable auto-renew? No further automatic charges will occur.",
"tribute_subscription_cancelled": "🚨 <b>Subscription Cancelled</b>\n\nYour Tribute subscription has been cancelled. You have 24 hours to restore access, after which the subscription will be blocked.\n\nTo renew your subscription, press the button below.",
"tribute_auto_renewal": "🔄 <b>Subscription Auto-Renewed</b>\n\nYour Tribute subscription has been automatically renewed for {months} months.\nNew expiration date: {end_date}",
"yookassa_auto_renewal": "🔄 <b>Subscription Auto-Renewed</b>\n\nYour subscription was automatically renewed for {months} month(s).\nNew expiration date: {end_date}", "yookassa_auto_renewal": "🔄 <b>Subscription Auto-Renewed</b>\n\nYour subscription was automatically renewed for {months} month(s).\nNew expiration date: {end_date}",
"admin_user_management_prompt": "👤 User Management\n\nEnter user ID or @username to search:", "admin_user_management_prompt": "👤 User Management\n\nEnter user ID or @username to search:",
"admin_user_subscription_info": "Subscription Information:", "admin_user_subscription_info": "Subscription Information:",
@@ -442,9 +439,6 @@
"payment_method_tx_history_title": "📜 Transactions history", "payment_method_tx_history_title": "📜 Transactions history",
"payment_method_no_history": "No transactions history.", "payment_method_no_history": "No transactions history.",
"subscription_purchase_title": "Subscription purchase for {months} mo.", "subscription_purchase_title": "Subscription purchase for {months} mo.",
"subscription_tribute_notice": "Paid via Tribute. Renew using your Tribute link.",
"subscription_tribute_notice_with_link": "Paid via Tribute. Renew: {link}",
"subscription_autorenew_not_supported_for_tribute": "Auto-renew is handled by Tribute. Manage renewal in the Tribute app/link.",
"autorenew_enable_requires_card": "Link a payment card in Payment Methods before enabling auto-renew.", "autorenew_enable_requires_card": "Link a payment card in Payment Methods before enabling auto-renew.",
"subscription_not_active": "You don't have an active subscription.", "subscription_not_active": "You don't have an active subscription.",
"error_service_unavailable": "Service unavailable. Please try again later.", "error_service_unavailable": "Service unavailable. Please try again later.",
-6
View File
@@ -41,7 +41,6 @@
"pay_with_severpay_button": "💳 SeverPay", "pay_with_severpay_button": "💳 SeverPay",
"back_to_payment_methods_button": "⬅️ Назад", "back_to_payment_methods_button": "⬅️ Назад",
"pay_with_cryptopay_button": "💎 CryptoBot", "pay_with_cryptopay_button": "💎 CryptoBot",
"pay_with_tribute_button": "❤️ Tribute",
"pay_with_stars_button": "🌟 Звезды Telegram", "pay_with_stars_button": "🌟 Звезды Telegram",
"connect_button": "🔗 Подключиться", "connect_button": "🔗 Подключиться",
@@ -169,7 +168,6 @@
"autorenew_48h_charge_tomorrow_notice": "🔔 Напоминание\n\nЗавтра будет автоматическое списание за продление подписки. Если вы не хотите автопродление — отключите его кнопкой ниже.", "autorenew_48h_charge_tomorrow_notice": "🔔 Напоминание\n\nЗавтра будет автоматическое списание за продление подписки. Если вы не хотите автопродление — отключите его кнопкой ниже.",
"autorenew_confirm_enable": "🔄 Включить автопродление? Перед окончанием подписки будет выполняться автосписание.", "autorenew_confirm_enable": "🔄 Включить автопродление? Перед окончанием подписки будет выполняться автосписание.",
"autorenew_confirm_disable": "🛑 Отключить автопродление? Автосписаний больше не будет.", "autorenew_confirm_disable": "🛑 Отключить автопродление? Автосписаний больше не будет.",
"tribute_subscription_cancelled": "🚨 <b>Подписка отменена</b>\n\nВаша подписка Tribute была отменена. У вас есть 24 часа для восстановления доступа, после чего подписка будет заблокирована.\n\nДля продления подписки нажмите кнопку ниже.",
"yookassa_auto_renewal": "🔄 <b>Подписка автоматически продлена</b>\n\nВаша подписка была автоматически продлена на {months} мес.\nНовая дата окончания: {end_date}", "yookassa_auto_renewal": "🔄 <b>Подписка автоматически продлена</b>\n\nВаша подписка была автоматически продлена на {months} мес.\nНовая дата окончания: {end_date}",
"admin_promo_set_validity_days": "⏰ Установить срок (дни)", "admin_promo_set_validity_days": "⏰ Установить срок (дни)",
"admin_back_to_panel": "⬅️ В панель", "admin_back_to_panel": "⬅️ В панель",
@@ -242,7 +240,6 @@
"admin_broadcast_invalid_html": "❌ Некорректный HTML в сообщении. Пожалуйста, отправьте корректный HTML (поддерживаются теги Telegram) или уберите теги.", "admin_broadcast_invalid_html": "❌ Некорректный HTML в сообщении. Пожалуйста, отправьте корректный HTML (поддерживаются теги Telegram) или уберите теги.",
"error_displaying_logs_too_long": "Ошибка: логи слишком длинные для отображения одним сообщением. Попробуйте найти логи по конкретному пользователю.", "error_displaying_logs_too_long": "Ошибка: логи слишком длинные для отображения одним сообщением. Попробуйте найти логи по конкретному пользователю.",
"error_displaying_statistics": "Ошибка отображения статистики.", "error_displaying_statistics": "Ошибка отображения статистики.",
"tribute_auto_renewal": "🔄 <b>Подписка автоматически продлена</b>\n\nВаша подписка Tribute была автоматически продлена на {months} мес.\nНовая дата окончания: {end_date}",
"admin_user_management_prompt": "👤 Управление пользователями\n\nВведите ID пользователя или @username для поиска:", "admin_user_management_prompt": "👤 Управление пользователями\n\nВведите ID пользователя или @username для поиска:",
"admin_user_subscription_info": "Информация о подписке:", "admin_user_subscription_info": "Информация о подписке:",
"admin_user_reset_trial_button": "🔄 Сбросить триал", "admin_user_reset_trial_button": "🔄 Сбросить триал",
@@ -442,9 +439,6 @@
"payment_method_tx_history_title": "📜 История операций", "payment_method_tx_history_title": "📜 История операций",
"payment_method_no_history": "История операций отсутствует.", "payment_method_no_history": "История операций отсутствует.",
"subscription_purchase_title": "Покупка подписки на {months} мес.", "subscription_purchase_title": "Покупка подписки на {months} мес.",
"subscription_tribute_notice": "Оплачено через Tribute. Продление делайте по ссылке Tribute.",
"subscription_tribute_notice_with_link": "Оплачено через Tribute. Продлить: {link}",
"subscription_autorenew_not_supported_for_tribute": "Автопродление управляется Tribute. Управляйте продлением в приложении/ссылке Tribute.",
"autorenew_enable_requires_card": "Прежде чем включать автоплатёж, привяжите карту в разделе «Способы оплаты».", "autorenew_enable_requires_card": "Прежде чем включать автоплатёж, привяжите карту в разделе «Способы оплаты».",
"subscription_not_active": "У вас нет активной подписки.", "subscription_not_active": "У вас нет активной подписки.",
"error_service_unavailable": "Сервис недоступен. Попробуйте позже.", "error_service_unavailable": "Сервис недоступен. Попробуйте позже.",